From d9942456c8e6788ec216c5bae120fc828a3702cf Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:45:43 -0500 Subject: [PATCH] up --- api/get_conversations.php | 37 ++++++++++++++----- conversations.php | 76 +++++++++++++++++++++++++++++++++------ services/BotService.php | 16 ++++----- 3 files changed, 102 insertions(+), 27 deletions(-) diff --git a/api/get_conversations.php b/api/get_conversations.php index d811d9b..fe51f5a 100644 --- a/api/get_conversations.php +++ b/api/get_conversations.php @@ -14,9 +14,14 @@ header('Access-Control-Allow-Headers: Content-Type'); try { $db = Database::getInstance(); + // Paginación: recibir page y limit desde query string (con límites razonables) + $limit = isset($_GET['limit']) ? intval($_GET['limit']) : 50; + $limit = max(1, min(200, $limit)); // máximo 200 por página + $page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1; + $offset = ($page - 1) * $limit; + // Obtener conversaciones agregadas por usuario: último mensaje + conteo de no leídos + avatar - $conversations = $db->fetchAll( - "SELECT + $sql = "SELECT u.id AS user_id, COALESCE(u.name, u.phone_number) AS name, u.phone_number, @@ -43,14 +48,19 @@ try { ) lm ON lm.user_id = u.id GROUP BY u.id ORDER BY lm.created_at DESC - LIMIT 500" - ); - + 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"); + $total = isset($totalRow['count']) ? intval($totalRow['count']) : 0; + // Si no hay datos, retornar array vacío if (empty($conversations)) { $conversations = []; } - + // Formatear datos para el frontend $conversations = array_map(function($conv) { return [ @@ -71,8 +81,19 @@ try { 'in_service_at' => $conv['in_service_at'] ?? null ]; }, $conversations); - - echo json_encode($conversations); + + // Indicar metadatos para la paginación + $hasMore = ($page * $limit) < $total; + + echo json_encode([ + 'success' => true, + 'data' => $conversations, + 'page' => $page, + 'limit' => $limit, + 'has_more' => (bool)$hasMore, + 'total' => $total + ]); + } catch (Exception $e) { error_log("Error in get_conversations.php: " . $e->getMessage()); diff --git a/conversations.php b/conversations.php index 70aadd9..fa54b0a 100644 --- a/conversations.php +++ b/conversations.php @@ -455,6 +455,9 @@
Cargando conversaciones...
+ @@ -534,7 +537,11 @@ this.currentConversationId = null; this.currentUserId = null; this.conversations = []; - this.conversations = []; + // Pagination state + this.conversationsPage = 1; + this.conversationsLimit = 50; + this.hasMoreConversations = true; + this.loadingConversations = false; this.init(); } @@ -671,6 +678,20 @@ document.getElementById('file-input').addEventListener('change', (e) => { this.handleFileSelect(e); }); + + // Carga paginada: botón "Cargar más" y scroll infinito + const loadMoreBtn = document.getElementById('load-more-btn'); + if (loadMoreBtn) { + loadMoreBtn.addEventListener('click', () => this.loadMoreConversations()); + } + const convList = document.getElementById('conversation-list'); + if (convList) { + convList.addEventListener('scroll', () => { + if (this.hasMoreConversations && !this.loadingConversations && (convList.scrollTop + convList.clientHeight >= convList.scrollHeight - 60)) { + this.loadMoreConversations(); + } + }); + } } setupAutoRefresh() { @@ -687,30 +708,63 @@ }, 10000); } - async loadConversations() { + async loadConversations(page = 1, append = false) { + if (this.loadingConversations) return; + this.loadingConversations = true; + const loadMoreContainer = document.getElementById('load-more-container'); + const loadMoreBtn = document.getElementById('load-more-btn'); + if (loadMoreBtn) loadMoreBtn.disabled = true; try { - const response = await fetch('api/get_conversations.php'); - const data = await response.json(); + const url = `api/get_conversations.php?page=${page}&limit=${this.conversationsLimit}`; + const resp = await fetch(url); + const data = await resp.json(); console.log('Respuesta get_conversations:', data); // Debug - - // Manejar nuevo formato de respuesta de API + + let items = []; + let hasMore = false; if (data && data.success && Array.isArray(data.data)) { - this.conversations = data.data; - this.renderConversations(); + items = data.data; + hasMore = !!data.has_more || (page * this.conversationsLimit) < (data.total || 0); } else if (Array.isArray(data)) { - // Retrocompatibilidad con formato anterior - this.conversations = data; - this.renderConversations(); + items = data; + hasMore = items.length === this.conversationsLimit; } else { console.error('Error cargando conversaciones: formato de datos inválido', data); alert('Error cargando conversaciones: ' + (data.error || 'formato de datos inválido')); + this.loadingConversations = false; + if (loadMoreBtn) loadMoreBtn.disabled = false; + return; + } + + if (append) { + this.conversations = this.conversations.concat(items); + } else { + this.conversations = items; + } + + this.hasMoreConversations = hasMore; + this.conversationsPage = page; + this.renderConversations(); + + if (loadMoreContainer) { + loadMoreContainer.style.display = this.hasMoreConversations ? 'block' : 'none'; } } catch (error) { console.error('Error loading conversations:', error); alert('Error cargando conversaciones: ' + error.message); + } finally { + this.loadingConversations = false; + if (loadMoreBtn) loadMoreBtn.disabled = false; } } + async loadMoreConversations() { + if (!this.hasMoreConversations || this.loadingConversations) return; + await this.loadConversations(this.conversationsPage + 1, true); + } + + + renderConversations() { const container = document.getElementById('conversation-list'); diff --git a/services/BotService.php b/services/BotService.php index 5e473d8..35e4843 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, "⚠️ 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*." ); } 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, "🔔 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) . "."); } catch (Exception $e) { // ignore } @@ -176,7 +176,7 @@ class BotService { } else { // Opcional: informar que el asesor ya respondió y no podemos enviar automáticas try { - //$this->whatsappService->sendTextMessage($phoneNumber, "🔕 Un asesor ya ha intervenido en la conversación, por favor espera su respuesta o utiliza *menu* para ver opciones."); + //$this->whatsappService->sendTextMessage($phoneNumber, "🔕 Un asesor ya ha intervenido en la conversación, por favor espera su respuesta o utiliza *MENU* para ver opciones."); } 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. Escribe *MENU* para ver las opciones disponibles.'); if (!$enabled || !$welcomeMessage) return; @@ -330,7 +330,7 @@ class BotService { 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, responde solo con el número de la opción deseada o la palabra *MENU* para iniciar nuevamente." ); error_log("[BotService] processMenuSelection - non-numeric response from user {$user['id']}: '{$messageText}'"); return; @@ -467,7 +467,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í tu mensaje. Escribe *MENU* para ver las opciones disponibles o *ASESOR* para obtener ayuda."; $this->whatsappService->sendTextMessage($phoneNumber, $defaultMessage); } @@ -511,7 +511,7 @@ class BotService { // 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. Te hemos puesto en espera por {$minutes} minutos. Si no hay respuesta, podrás 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()); @@ -537,7 +537,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, "🔔 Tu conversación ha sido retomada por el equipo. Puedes usar *MENU* para continuar."); if (function_exists('writeLog')) writeLog('INFO', "Advisor released hold for $phoneNumber"); } catch (Exception $e) { error_log('releaseHold failed: ' . $e->getMessage());