up
This commit is contained in:
@@ -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
@@ -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');
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user