This commit is contained in:
Lizandro Guarnizo
2026-01-23 20:45:43 -05:00
parent 2aed2c428c
commit d9942456c8
3 changed files with 102 additions and 27 deletions
+29 -8
View File
@@ -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());
+65 -11
View File
@@ -455,6 +455,9 @@
<div>Cargando conversaciones...</div>
</div>
</div>
<div id="load-more-container" style="padding:10px; text-align:center; display:none;">
<button id="load-more-btn" class="btn btn-sm btn-outline-primary">Cargar más</button>
</div>
</div>
<!-- Área principal del chat -->
@@ -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');
+8 -8
View File
@@ -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());