@@ -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());