From 2017c8c8056851a08c212768c4eccec548e61b01 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:12:27 -0500 Subject: [PATCH] mejoras --- api/get_conversation_detail.php | 2 +- api/get_conversations.php | 19 ++- api/get_user_messages.php | 63 ++++++---- api/mark_conversation_unread.php | 31 +++++ chat_window.php | 121 +++++++++++++++++-- conversations.php | 196 +++++++++++++++++++++++++------ services/BotService.php | 80 +++++++++---- services/BotServiceLab.php | 18 +-- 8 files changed, 420 insertions(+), 110 deletions(-) create mode 100644 api/mark_conversation_unread.php diff --git a/api/get_conversation_detail.php b/api/get_conversation_detail.php index 619cf16..3eb0d27 100644 --- a/api/get_conversation_detail.php +++ b/api/get_conversation_detail.php @@ -116,7 +116,7 @@ try { 'mime_type' => $msg['mime_type'] ?? null, 'status' => $msg['status'] ?? 'sent', 'created_at' => $msg['created_at'], - 'time' => date('H:i', strtotime($msg['created_at'])), + 'time' => date('g:i a', strtotime($msg['created_at'])), 'date' => date('d/m/Y', strtotime($msg['created_at'])) ]; }, $conversations); diff --git a/api/get_conversations.php b/api/get_conversations.php index fe51f5a..d6807fb 100644 --- a/api/get_conversations.php +++ b/api/get_conversations.php @@ -21,6 +21,8 @@ try { $offset = ($page - 1) * $limit; // Obtener conversaciones agregadas por usuario: último mensaje + conteo de no leídos + avatar + $filter = isset($_GET['filter']) ? strtolower(trim($_GET['filter'])) : 'all'; + $sql = "SELECT u.id AS user_id, COALESCE(u.name, u.phone_number) AS name, @@ -46,14 +48,23 @@ try { SELECT user_id, MAX(created_at) AS last_time FROM conversations GROUP BY user_id ) t2 ON t1.user_id = t2.user_id AND t1.created_at = t2.last_time ) lm ON lm.user_id = u.id - GROUP BY u.id - ORDER BY lm.created_at DESC - LIMIT %d OFFSET %d"; + GROUP BY u.id"; + + // Aplicar filtro 'unread' si se solicita + if ($filter === 'unread') { + $sql .= " HAVING IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) > 0"; + } + + $sql .= "\n ORDER BY lm.created_at DESC\n LIMIT %d OFFSET %d"; $conversations = $db->fetchAll(sprintf($sql, $limit, $offset)); // Conteo total de usuarios con al menos una conversación (útil para paginar) - $totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations"); + if ($filter === 'unread') { + $totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations c WHERE c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0)"); + } else { + $totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations"); + } $total = isset($totalRow['count']) ? intval($totalRow['count']) : 0; // Si no hay datos, retornar array vacío diff --git a/api/get_user_messages.php b/api/get_user_messages.php index 9b96216..fa64ff2 100644 --- a/api/get_user_messages.php +++ b/api/get_user_messages.php @@ -22,12 +22,16 @@ try { echo json_encode(['error' => 'user_id es requerido']); exit; } - + + // Paginación: limit y before (timestamp) - cargamos N mensajes anteriores a 'before' + $limit = isset($_GET['limit']) ? intval($_GET['limit']) : 50; + $limit = max(1, min(200, $limit)); + $before = !empty($_GET['before']) ? $_GET['before'] : null; // expect timestamp string + $db = Database::getInstance(); - - // Obtener mensajes del usuario (de ambas tablas para compatibilidad) - $conversations = $db->fetchAll( - "SELECT + + // Construir consulta: obtener mensajes ordenados DESC (más recientes primero) y limitar + $sql = "SELECT id, user_id, COALESCE(content, message_text) as content, @@ -41,25 +45,29 @@ try { COALESCE(c.is_read, 0) as is_read FROM conversations c LEFT JOIN users u ON c.user_id = u.id - WHERE user_id = :user_id - UNION ALL - SELECT - id, - user_id, - message_text as content, - NULL as message_id, - NULL as media_url, - NULL as user_phone, - direction, - message_type, - status, - created_at, - 1 as is_read - FROM conversations - WHERE user_id = :user_id - ORDER BY created_at ASC", - ['user_id' => $userId] - ); + WHERE user_id = :user_id"; + + $params = ['user_id' => $userId]; + if ($before) { + $sql .= " AND created_at < :before"; + $params['before'] = $before; + } + + $sql .= " ORDER BY created_at DESC LIMIT " . $limit; + + $conversations = $db->fetchAll($sql, $params); + + // Si no hay mensajes, devolver array vacío + if (empty($conversations)) { + echo json_encode(['success' => true, 'data' => [], 'has_more' => false]); + exit; + } + + // Queremos devolver los mensajes en orden cronológico ascendente para la UI + $conversations = array_reverse($conversations); + + // Indicar si hay más mensajes anteriores (si la consulta original devolvió exactamente $limit, podría haber más) + $hasMore = count($conversations) >= 1 && count($conversations) == $limit ? true : false; // Si no hay mensajes en conversations, intentar con conversations if (empty($conversations)) { @@ -115,8 +123,11 @@ try { 'created_at' => $msg['created_at'] ]; }, $conversations); - - echo json_encode($conversations); + + // earliest message timestamp (para paginación hacia atrás) + $earliest = $conversations[0]['created_at'] ?? null; + + echo json_encode(['success' => true, 'data' => $conversations, 'has_more' => $hasMore, 'earliest' => $earliest]); } catch (Exception $e) { error_log("Error in get_user_conversations.php: " . $e->getMessage()); diff --git a/api/mark_conversation_unread.php b/api/mark_conversation_unread.php new file mode 100644 index 0000000..e28122a --- /dev/null +++ b/api/mark_conversation_unread.php @@ -0,0 +1,31 @@ + false, 'error' => 'user_id is required']); + exit; + } + + $db = Database::getInstance(); + // Marcar como no leídos todos los mensajes entrantes del usuario + $db->execute("UPDATE conversations SET is_read = 0 WHERE user_id = ? AND direction = 'incoming'", [$userId]); + + echo json_encode(['success' => true]); +} catch (Exception $e) { + error_log('mark_conversation_unread failed: ' . $e->getMessage()); + http_response_code(500); + echo json_encode(['success' => false, 'error' => 'Internal server error']); +} +?> \ No newline at end of file diff --git a/chat_window.php b/chat_window.php index 864bd82..326d01d 100644 --- a/chat_window.php +++ b/chat_window.php @@ -73,11 +73,73 @@ if (empty($user_id)) { .message-bubble { margin: 6px 0; padding: 6px 10px; /* menos padding */ - border-radius: 12px; - max-width: 70%; + border-radius: 18px; + max-width: 78%; word-wrap: break-word; font-size: 0.88rem; + position: relative; /* para posicionar timestamp */ + padding-right: 60px; /* espacio para timestamp y ticks */ } + + /* Estilos tipo WhatsApp: colas y timestamps dentro de burbuja */ + .message-bubble.message-outgoing { + border-bottom-right-radius: 4px; /* más afilado en la esquina del tail */ + } + .message-bubble.message-incoming { + border-bottom-left-radius: 4px; + } + + /* Cola simple usando pseudo-elemento */ + .message-bubble.message-outgoing::after { + content: ''; + position: absolute; + right: -6px; + bottom: 4px; + width: 12px; + height: 12px; + background: #DCF8C6; + transform: rotate(45deg); + border-bottom-right-radius: 2px; + z-index: 0; + } + .message-bubble.message-incoming::after { + content: ''; + position: absolute; + left: -6px; + bottom: 4px; + width: 12px; + height: 12px; + background: #fff; + transform: rotate(45deg); + border-bottom-left-radius: 2px; + z-index: 0; + box-shadow: 0 1px 2px rgba(0,0,0,0.03); + } + + /* Timestamp dentro de la burbuja, oculto hasta hover para limpieza visual */ + .message-bubble .message-time { + position: absolute; + right: 8px; + bottom: 4px; + font-size: 0.65rem; + color: #666; + opacity: 0; + transition: opacity 0.12s ease-in-out; + white-space: nowrap; + } + .message-bubble:hover .message-time, + .message-bubble:focus-within .message-time { + opacity: 1; + } + + /* Agrupación de mensajes consecutivos (menor separación) */ + .message-bubble.grouped { margin-top: 2px; } + .message-bubble.grouped + .message-bubble { margin-top: 2px; } + .message-bubble.grouped::after { bottom: 2px; } + .message-bubble.message-alert { padding-right: 60px; } + + /* Ajuste para que texto no choque con timestamp */ + .message-text { display: block; padding-right: 6px; } /* Estilos para mensajes multimedia */ .message-bubble img { @@ -150,6 +212,13 @@ if (empty($user_id)) { .message-incoming .message-time { text-align: left; } + + /* Preservar espacios y saltos de línea en el texto de mensajes */ + .message-text { + white-space: pre-wrap; /* conserva saltos de línea y múltiples espacios */ + word-break: break-word; + overflow-wrap: anywhere; + } .typing-indicator { display: none; @@ -772,9 +841,23 @@ if (empty($user_id)) { } let html = ''; - conversations.forEach(msg => { + for (let i = 0; i < conversations.length; i++) { + const msg = conversations[i]; + const prev = conversations[i-1] || null; const isOutgoing = msg.direction === 'outgoing'; let messageClass = isOutgoing ? 'message-outgoing' : 'message-incoming'; + // Agrupar si el mensaje anterior es del mismo lado y está cerca en el tiempo (5 min) + try { + if (prev && prev.direction === msg.direction) { + const t1 = new Date(prev.created_at).getTime(); + const t2 = new Date(msg.created_at).getTime(); + if (!isNaN(t1) && !isNaN(t2) && (Math.abs(t2 - t1) <= (5 * 60 * 1000))) { + messageClass += ' grouped'; + } + } + } catch (e) { + // ignore parsing errors + } // Debug: ver todos los datos del mensaje (prioriza media_url_external y valida URLs) const has_media_external = isValidMediaUrl(msg.media_url_external); @@ -974,29 +1057,45 @@ if (empty($user_id)) { } if (alertInfo) { - contentHtml = `
${escapeHtml(alertInfo.text || '[Atención]')}
`; + contentHtml = `
${escapeHtml(alertInfo.text || '[Atención]')}
`; // Añadir clase especial a la burbuja // marca el mensaje como requiring attention mediante messageClass // we'll append an additional class later messageClass += ' message-alert'; } else { - contentHtml = escapeHtml(msg.content) || 'Sin contenido'; + const textContent = msg.content || ''; + contentHtml = textContent ? `
${escapeHtml(textContent)}
` : 'Sin contenido'; } } const messageTime = escapeHtml(msg.time || ''); - + + // Status ticks (solo en salientes) + let statusHtml = ''; + if (isOutgoing) { + const s = (msg.status || '').toLowerCase(); + if (s === 'read' || s === 'seen') { + statusHtml = ''; + } else if (s === 'delivered') { + statusHtml = ''; + } else if (s === 'sent') { + statusHtml = ''; + } else { + statusHtml = ''; + } + } + html += `
${contentHtml}
- ${messageTime} ${isOutgoing ? '' : ''} + ${messageTime} ${statusHtml}
`; - }); + } chatconversations.innerHTML = html; scrollToBottom(); @@ -1167,13 +1266,13 @@ if (empty($user_id)) { const messageHtml = `
-
${escapeHtml(message)}
+
${escapeHtml(message)}
${now} ${isOutgoing ? '' : ''}
- `; + `; chatconversations.insertAdjacentHTML('beforeend', messageHtml); scrollToBottom(); @@ -1723,7 +1822,7 @@ if (empty($user_id)) { let html = `
${mediaHtml}
`; if (message.caption) { - html += `
${escapeHtml(message.caption)}
`; + html += `
${escapeHtml(message.caption)}
`; } if (timestamp) { diff --git a/conversations.php b/conversations.php index fa54b0a..d4e370b 100644 --- a/conversations.php +++ b/conversations.php @@ -448,6 +448,15 @@ + + +
+ + Mostrar sólo conversaciones con mensajes no leídos +
@@ -488,6 +497,7 @@
+
@@ -537,7 +547,14 @@ this.currentConversationId = null; this.currentUserId = null; this.conversations = []; - // Pagination state + // Message pagination / loading state + this.messageLimit = 50; + this.loadingMessages = false; + this.hasMoreMessages = false; + this.earliestMessage = null; // timestamp of earliest loaded message + // Filter state: 'all' or 'unread' + this.conversationFilter = 'all'; + // Pagination state for conversation list this.conversationsPage = 1; this.conversationsLimit = 50; this.hasMoreConversations = true; @@ -692,6 +709,17 @@ } }); } + + // Filtro: Todos / No leídos + const filterSelect = document.getElementById('conversation-filter'); + if (filterSelect) { + filterSelect.value = this.conversationFilter; + filterSelect.addEventListener('change', (e) => { + this.conversationFilter = e.target.value || 'all'; + // reload conversations from first page + this.loadConversations(1, false); + }); + } } setupAutoRefresh() { @@ -715,7 +743,7 @@ const loadMoreBtn = document.getElementById('load-more-btn'); if (loadMoreBtn) loadMoreBtn.disabled = true; try { - const url = `api/get_conversations.php?page=${page}&limit=${this.conversationsLimit}`; + const url = `api/get_conversations.php?page=${page}&limit=${this.conversationsLimit}&filter=${encodeURIComponent(this.conversationFilter)}`; const resp = await fetch(url); const data = await resp.json(); console.log('Respuesta get_conversations:', data); // Debug @@ -769,10 +797,11 @@ const container = document.getElementById('conversation-list'); if (this.conversations.length === 0) { + const emptyMsg = this.conversationFilter === 'unread' ? 'No hay conversaciones no leídas' : 'No hay conversaciones aún'; container.innerHTML = `
-

No hay conversaciones aún

+

${emptyMsg}

`; return; @@ -901,10 +930,35 @@ const btn = document.getElementById('bot-toggle'); const holdIndicator = document.getElementById('hold-indicator'); const releaseBtn = document.getElementById('release-hold-btn'); + const markUnreadBtn = document.getElementById('mark-unread-btn'); - if (holdIndicator) { - // Show different labels depending on state - if (conv.on_hold) { + if (markUnreadBtn) { + markUnreadBtn.style.display = 'inline-block'; + markUnreadBtn.onclick = async () => { + try { + const resp = await fetch('api/mark_conversation_unread.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_id: userId }) + }); + if (resp.status === 401) { + alert('Sesión expirada. Por favor inicie sesión de nuevo.'); + return; + } + const json = await resp.json(); + if (json && json.success) { + // Recargar lista de conversaciones para reflejar cambios + await this.loadConversations(); + alert('Conversación marcada como NO leída.'); + } else { + alert('Error marcando conversación como no leída'); + } + } catch (e) { + console.error('Error marking conversation unread', e); + alert('Error marcando conversación como no leída'); + } + }; + } holdIndicator.textContent = 'EN ESPERA'; holdIndicator.style.color = '#b85'; holdIndicator.style.display = 'inline'; @@ -1051,13 +1105,26 @@ } } - // Cargar mensajes - await this.loadconversations(userId); + // Cargar mensajes (con paginación) + await this.loadMessages(userId, true); + // Añadir listener para scroll arriba (cargar más historial) + const chatContainer = document.getElementById('chat-conversations'); + if (chatContainer) { + if (!chatContainer._infiniteScrollAdded) { + chatContainer.addEventListener('scroll', async () => { + if (chatContainer.scrollTop <= 60 && this.hasMoreMessages && !this.loadingMessages && this.currentUserId == userId) { + await this.loadMessages(userId, false); + } + }); + chatContainer._infiniteScrollAdded = true; + } + } // Cargar respuestas rápidas await this.loadQuickReplies(); } async loadconversations(userId, showLoading = true) { + // Compatibilidad: ahora delegamos en loadMessages if (showLoading) { document.getElementById('chat-conversations').innerHTML = `
@@ -1067,36 +1134,9 @@ } try { - // Usar apiCall para incluir debug=true y manejo de auth/errores - const data = await this.apiCall(`get_user_conversations.php?user_id=${userId}`); - - let messages = []; - if (!data) { - throw new Error('No autorizado o error en la petición'); - } - - if (Array.isArray(data)) { - messages = data; - } else if (data && data.success && Array.isArray(data.data)) { - messages = data.data; - } else if (Array.isArray(data.conversations)) { - messages = data.conversations; - } - - this.conversations = messages; - this.renderconversations(); - this.scrollToBottom(); - - // Marcar como leídos en el backend - try { - await this.apiCall('mark_conversation_read.php', { method: 'POST', body: { user_id: userId } }); - // Recargar lista de conversaciones para refrescar contadores - await this.loadConversations(); - } catch (e) { - console.error('Error marking conversation as read', e); - } + await this.loadMessages(userId, true); } catch (error) { - console.error('Error loading conversations:', error); + console.error('Error loading conversations via loadMessages:', error); document.getElementById('chat-conversations').innerHTML = `
@@ -1106,6 +1146,88 @@ } } + /** + * Cargar mensajes con paginación. Si initial=true, carga el bloque más reciente; si initial=false, carga mensajes anteriores (before=this.earliestMessage). + */ + async loadMessages(userId, initial = false) { + if (this.loadingMessages) return; + this.loadingMessages = true; + const container = document.getElementById('chat-conversations'); + + // si cargamos más antiguos, preservar scroll + let prevScrollHeight = container ? container.scrollHeight : 0; + let prevScrollTop = container ? container.scrollTop : 0; + + // indicador top cuando cargamos anteriores + let topLoader = null; + if (!initial && container) { + container.insertAdjacentHTML('afterbegin', `
Cargando mensajes anteriores...
`); + topLoader = container.querySelector('.loading-top'); + } + + try { + let url = `get_user_conversations.php?user_id=${userId}&limit=${this.messageLimit}`; + if (!initial && this.earliestMessage) { + url += `&before=${encodeURIComponent(this.earliestMessage)}`; + } + + const data = await this.apiCall(url); + if (!data) throw new Error('No autorizado o error en la petición'); + + let messages = []; + let hasMore = false; + let earliest = null; + if (data && data.success && Array.isArray(data.data)) { + messages = data.data; + hasMore = !!data.has_more; + earliest = data.earliest || (messages[0] && messages[0].created_at) || null; + } else if (Array.isArray(data)) { + messages = data; + } + + if (initial) { + this.conversations = messages; + } else { + // Prepend mensajes antiguos + this.conversations = messages.concat(this.conversations); + } + + // Actualizar estado de paginación + this.hasMoreMessages = hasMore; + if (earliest) this.earliestMessage = earliest; + + // Renderizar y ajustar scroll + this.renderconversations(); + + if (initial) { + this.scrollToBottom(); + } else if (container) { + // Mantener posición: desplazar por la diferencia de heights + const newScrollHeight = container.scrollHeight; + container.scrollTop = newScrollHeight - prevScrollHeight + prevScrollTop; + } + + // remover loader top si existía + if (topLoader && topLoader.parentNode) topLoader.remove(); + + // Marcar como leídos (comportamiento previo: marcar todos los entrantes como leídos al abrir) + if (initial) { + try { + await this.apiCall('mark_conversation_read.php', { method: 'POST', body: { user_id: userId } }); + // Recargar lista de conversaciones para refrescar contadores + await this.loadConversations(); + } catch (e) { + console.error('Error marking conversation as read', e); + } + } + + } catch (error) { + console.error('Error en loadMessages:', error); + } finally { + this.loadingMessages = false; + } + } + renderconversations() { const container = document.getElementById('chat-conversations'); diff --git a/services/BotService.php b/services/BotService.php index 35e4843..cfa4916 100644 --- a/services/BotService.php +++ b/services/BotService.php @@ -109,7 +109,7 @@ class BotService { // Si hay un tiempo de expiración y aún no pasó, notificar y retornar if ($until && $until > $now) { try { - $this->whatsappService->sendTextMessage($phoneNumber, "⚠️ Has sido puesto en espera por un asesor. Estarás en espera hasta " . date('H:i', $until) . ". Si no hay respuesta, podrás usar *MENU*." ); + $this->whatsappService->sendTextMessage($phoneNumber, "⚠️ Se le ha puesto en espera por un asesor. Estará en espera hasta " . date('g:i a', $until) . ". Si no hay respuesta, podrá usar *MENU*." ); } catch (Exception $e) { // ignore } @@ -127,7 +127,7 @@ class BotService { $until = !empty($user['bot_paused_until']) ? strtotime($user['bot_paused_until']) : null; if ($until && $until > $now) { try { - $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Tu solicitud de asesor está pendiente. Un miembro del equipo te atenderá en breve. Si no, podrás usar *MENU* después de " . date('H:i', $until) . "."); + $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Su solicitud de asesor está pendiente. Un miembro del equipo le atenderá en breve. Si no hay respuesta, podrá usar *MENU* después de " . date('g:i a', $until) . "."); } catch (Exception $e) { // ignore } @@ -203,7 +203,7 @@ class BotService { */ public function sendWelcomeMessage($phoneNumber) { $enabled = getConfigFromDB('welcome_enabled', '1'); - $welcomeMessage = getConfigFromDB('welcome_message', '¡Hola! 👋 Bienvenido a nuestro servicio automatizado. Escribe *MENU* para ver las opciones disponibles.'); + $welcomeMessage = getConfigFromDB('welcome_message', '¡Hola! 👋 Bienvenido a nuestro servicio automatizado. Escriba *MENU* para ver las opciones disponibles.'); if (!$enabled || !$welcomeMessage) return; @@ -238,8 +238,8 @@ class BotService { case 'asesor': case 'ayuda': - // Solicitar asesor: marcar solicitud y pausar brevemente (5 minutos) - $this->requestAdvisor($phoneNumber, 5); + // Solicitar asesor: marcar solicitud y pausar brevemente (3 minutos) + $this->requestAdvisor($phoneNumber, 3); return true; } @@ -259,7 +259,7 @@ class BotService { } else { $this->whatsappService->sendTextMessage( $phoneNumber, - "❌ No hay menús configurados. Contacta con soporte." + "❌ No hay menús configurados. Contacte con soporte." ); } } @@ -307,7 +307,12 @@ class BotService { $menuText .= $option['option_number'] . ". " . $option['text'] . "\n"; } - $menuText .= "\n💬 *Responde con el número de la opción que deseas*"; + // Añadir opción "Atrás" si el menú tiene un menú padre + if (!empty($menu['parent_id'])) { + $menuText .= "0. Atrás\n"; + } + + $menuText .= "\n💬 *Responda con el número de la opción que desea*"; // Actualizar estado del usuario $this->updateUserMenuState($phoneNumber, $menuId); @@ -326,17 +331,30 @@ class BotService { // DEBUG try { error_log("[BotService] processMenuSelection - user_id={$user['id']} menu_id={$currentMenuId} received='{$messageText}'"); } catch (Throwable $t) {} + // Soportar comando "atrás" o "volver" para navegar al menú padre + $cmd = mb_strtolower(trim($messageText)); + $backCommands = ['atras', 'atrás', 'volver', 'back']; + if (in_array($cmd, $backCommands, true)) { + $this->goToParentMenu($phoneNumber, $currentMenuId); + return; + } + // Verificar si es un número if (!is_numeric($messageText)) { $this->whatsappService->sendTextMessage( $phoneNumber, - "❌ Por favor, responde solo con el número de la opción deseada o la palabra *MENU* para iniciar nuevamente." + "❌ Por favor, responda solo con el número de la opción deseada, la palabra *MENU* o *ATRÁS* para volver." ); error_log("[BotService] processMenuSelection - non-numeric response from user {$user['id']}: '{$messageText}'"); return; } $optionNumber = (int)$messageText; + // Si eligió 0 => volver al menú padre + if ($optionNumber === 0) { + $this->goToParentMenu($phoneNumber, $currentMenuId); + return; + } // Buscar la opción seleccionada por número $option = $this->db->fetch( @@ -361,7 +379,7 @@ class BotService { if (!$option) { $this->whatsappService->sendTextMessage( $phoneNumber, - "❌ Opción inválida. Por favor, selecciona una opción válida del menú." + "❌ Opción inválida. Por favor, seleccione una opción válida del menú." ); error_log("[BotService] processMenuSelection - no option found for user {$user['id']} menu={$currentMenuId} option={$optionNumber}"); return; @@ -410,8 +428,8 @@ class BotService { $message = $option['action_value'] ?: "Gracias por usar nuestro servicio. ¡Hasta pronto!"; $this->whatsappService->sendTextMessage($phoneNumber, $message); $this->exitMenu($phoneNumber); - // Pausar el bot para esta conversación por 5 minutos (antes eran 4 horas) - $this->pauseConversationForMinutes($phoneNumber, 5); + // Pausar el bot para esta conversación por 3 minutos (antes eran 4 horas) + $this->pauseConversationForMinutes($phoneNumber, 3); break; } } @@ -424,7 +442,7 @@ class BotService { // Por ejemplo, consultar saldos, procesar pagos, etc. $this->whatsappService->sendTextMessage( $phoneNumber, - "🔄 Procesando tu solicitud... Un momento por favor." + "🔄 Procesando su solicitud... Un momento, por favor." ); // Simular procesamiento @@ -432,7 +450,7 @@ class BotService { $this->whatsappService->sendTextMessage( $phoneNumber, - "✅ Tu solicitud ha sido procesada exitosamente." + "✅ Su solicitud ha sido procesada con éxito." ); $this->exitMenu($phoneNumber); @@ -467,7 +485,7 @@ class BotService { * Enviar mensaje por defecto cuando no hay coincidencias */ private function sendDefaultNoMatch($phoneNumber) { - $defaultMessage = "🤖 No entendí tu mensaje. Escribe *MENU* para ver las opciones disponibles o *ASESOR* para obtener ayuda."; + $defaultMessage = "🤖 No entendí su mensaje. Escriba *MENU* para ver las opciones disponibles o *ASESOR* para obtener ayuda."; $this->whatsappService->sendTextMessage($phoneNumber, $defaultMessage); } @@ -486,7 +504,7 @@ class BotService { /** * Poner la conversación en pausa por X minutos (llamada desde opciones 'end') */ - private function pauseConversationForMinutes($phoneNumber, $minutes = 5) { + private function pauseConversationForMinutes($phoneNumber, $minutes = 3) { try { $pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes")); $this->db->update('users', ['bot_paused_until' => $pausedUntil], 'phone_number = :phone', ['phone' => $phoneNumber]); @@ -505,13 +523,13 @@ class BotService { /** * Poner conversación en espera (asesor solicitado) por X minutos */ - public function putOnHold($phoneNumber, $minutes = 5) { + public function putOnHold($phoneNumber, $minutes = 3) { try { $pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes")); // Guardar bandera y tiempo de expiración (usamos bot_paused_until que ya existe) $this->db->update('users', ['on_hold' => 1, 'bot_paused_until' => $pausedUntil, 'advisor_requested' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]); // Enviar mensaje (bot) marcando la solicitud; no incluimos metadata de operador para evitar limpiar la marca - $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Se ha solicitado un asesor. Te hemos puesto en espera por {$minutes} minutos. Si no hay respuesta, podrás volver a usar *MENU*." ); + $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Se ha solicitado un asesor. Se le ha puesto en espera por {$minutes} minutos. Si no hay respuesta, podrá volver a usar *MENU*." ); if (function_exists('writeLog')) writeLog('INFO', "Advisor requested for $phoneNumber until $pausedUntil"); } catch (Exception $e) { error_log('putOnHold failed: ' . $e->getMessage()); @@ -521,12 +539,12 @@ class BotService { /** * Registrar solicitud de asesor sin poner on_hold (usuario lo solicita, se notifica al equipo) */ - public function requestAdvisor($phoneNumber, $minutes = 5) { + public function requestAdvisor($phoneNumber, $minutes = 3) { try { $pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes")); // No ponemos on_hold para evitar bloqueo automático; usamos advisor_requested $this->db->update('users', ['advisor_requested' => 1, 'bot_paused_until' => $pausedUntil], 'phone_number = :phone', ['phone' => $phoneNumber]); - $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Te hemos transferido con un asesor, te responderemos en breve."); + $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Le hemos transferido a un asesor; le responderemos en breve."); if (function_exists('writeLog')) writeLog('INFO', "Advisor solicited for $phoneNumber until $pausedUntil"); } catch (Exception $e) { @@ -537,7 +555,7 @@ class BotService { public function releaseHold($phoneNumber) { try { $this->db->update('users', ['on_hold' => 0, 'bot_paused_until' => null, 'advisor_requested' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]); - $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Tu conversación ha sido retomada por el equipo. Puedes usar *MENU* para continuar."); + $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Su conversación ha sido retomada por el equipo. Puede usar *MENU* para continuar."); if (function_exists('writeLog')) writeLog('INFO', "Advisor released hold for $phoneNumber"); } catch (Exception $e) { error_log('releaseHold failed: ' . $e->getMessage()); @@ -556,8 +574,7 @@ class BotService { // Notificar al usuario try { - $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Un asesor está atendiendo tu conversación. Por favor, espera su respuesta."); - } catch (Exception $e) { + $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Un asesor está atendiendo su conversación. Por favor, espere su respuesta."); // ignore send errors } @@ -629,6 +646,25 @@ class BotService { } } + /** + * Ir al menú padre del menú actual + */ + private function goToParentMenu($phoneNumber, $currentMenuId) { + try { + $menu = $this->db->fetch("SELECT parent_id FROM menus WHERE id = :id", ['id' => $currentMenuId]); + $parentId = $menu['parent_id'] ?? null; + if (!empty($parentId)) { + $this->showMenu($phoneNumber, $parentId); + } else { + // Si no hay padre, mostrar menú principal + $this->showMainMenu($phoneNumber); + } + } catch (Exception $e) { + error_log('[BotService] goToParentMenu failed: ' . $e->getMessage()); + $this->showMainMenu($phoneNumber); + } + } + /** * Actualizar estado del menú del usuario */ diff --git a/services/BotServiceLab.php b/services/BotServiceLab.php index f7bff87..db5305c 100644 --- a/services/BotServiceLab.php +++ b/services/BotServiceLab.php @@ -81,7 +81,7 @@ class BotServiceLab } catch (Exception $e) { error_log("Error en BotServiceLab->processMessage: " . $e->getMessage()); - $this->sendMessage($phoneNumber, "Lo siento, ocurrió un error. Por favor intenta de nuevo o escribe *ASESOR* para ayuda."); + $this->sendMessage($phoneNumber, "Lo siento, ocurrió un error. Por favor, intente de nuevo o escriba *ASESOR* para ayuda."); } } @@ -99,7 +99,7 @@ class BotServiceLab if (!$response) { // No se encontró respuesta, enviar mensaje por defecto - $this->sendMessage($phoneNumber, "❓ No he comprendido tu mensaje.\n\nEscribe *MENÚ* para ver las opciones disponibles."); + $this->sendMessage($phoneNumber, "❓ No he comprendido su mensaje.\n\nEscriba *MENÚ* para ver las opciones disponibles."); return; } @@ -180,13 +180,13 @@ class BotServiceLab if ($messageType === 'image' || $messageType === 'document') { $this->handleUploadedDocument($user, $mediaId, $currentState['state']); } else { - $this->sendMessage($phoneNumber, "Por favor envía una imagen o documento PDF."); + $this->sendMessage($phoneNumber, "Por favor, envíe una imagen o documento PDF."); } return; } // Si no está esperando media, informar - $this->sendMessage($phoneNumber, "He recibido tu archivo. ¿En qué puedo ayudarte?\n\nEscribe *MENÚ* para ver las opciones."); + $this->sendMessage($phoneNumber, "He recibido su archivo. ¿En qué puedo ayudarle?\n\nEscriba *MENÚ* para ver las opciones."); } /** @@ -204,7 +204,7 @@ class BotServiceLab $mediaUrl = $this->whatsappService->downloadMedia($mediaId); if (!$mediaUrl) { - $this->sendMessage($phoneNumber, "❌ No pude descargar el archivo. Por favor intenta de nuevo."); + $this->sendMessage($phoneNumber, "❌ No pude descargar el archivo. Por favor, intente de nuevo."); return; } @@ -215,7 +215,7 @@ class BotServiceLab 'order_received_at' => date('Y-m-d H:i:s') ]); - $this->sendMessage($phoneNumber, "✅ Orden médica recibida.\n\nAhora necesito los datos del paciente:\n\n✓ Nombre completo:\n✓ Número de documento:\n✓ Dirección completa:\n✓ Número de celular:\n✓ Correo electrónico:\n✓ ¿Es particular o por seguro?\n\nEnvíalos en UN SOLO mensaje."); + $this->sendMessage($phoneNumber, "✅ Orden médica recibida.\n\nAhora necesito los datos del paciente:\n\n✓ Nombre completo:\n✓ Número de documento:\n✓ Dirección completa:\n✓ Número de celular:\n✓ Correo electrónico:\n✓ ¿Es particular o por seguro?\n\nEnvíelos en UN SOLO mensaje."); $this->stateService->setState($phoneNumber, ConversationStateService::STATE_AWAITING_PATIENT_DATA); @@ -225,7 +225,7 @@ class BotServiceLab 'id_received_at' => date('Y-m-d H:i:s') ]); - $this->sendMessage($phoneNumber, "✅ Documento de identidad recibido.\n\nProcesando tu solicitud..."); + $this->sendMessage($phoneNumber, "✅ Documento de identidad recibido.\n\nProcesando su solicitud..."); // Aquí iría lógica adicional según el flujo $this->stateService->setState($phoneNumber, ConversationStateService::STATE_VALIDATING_DATA); @@ -233,7 +233,7 @@ class BotServiceLab } catch (Exception $e) { error_log("Error downloading media: " . $e->getMessage()); - $this->sendMessage($phoneNumber, "❌ Hubo un problema al procesar el archivo. Por favor intenta nuevamente."); + $this->sendMessage($phoneNumber, "❌ Hubo un problema al procesar el archivo. Por favor, intente nuevamente."); } } @@ -306,7 +306,7 @@ class BotServiceLab $result = $this->whatsappService->sendTemplateMessage($phoneNumber, $templateName, $parameters); if (!$result) { - $this->sendMessage($phoneNumber, "❌ No pude enviar la plantilla. Por favor contacta a un asesor."); + $this->sendMessage($phoneNumber, "❌ No pude enviar la plantilla. Por favor, contacte a un asesor."); } } catch (Exception $e) { error_log("Error sending template: " . $e->getMessage());