mejoras
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
+37
-26
@@ -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());
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
* API - Marcar conversación como NO LEÍDA (set is_read = 0)
|
||||
*/
|
||||
require_once '../config/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
try {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$userId = isset($input['user_id']) ? intval($input['user_id']) : 0;
|
||||
if (!$userId) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => 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']);
|
||||
}
|
||||
?>
|
||||
+110
-11
@@ -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 = `<div class="fw-bold">${escapeHtml(alertInfo.text || '[Atención]')}</div>`;
|
||||
contentHtml = `<div class="message-text fw-bold">${escapeHtml(alertInfo.text || '[Atención]')}</div>`;
|
||||
// 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) || '<em>Sin contenido</em>';
|
||||
const textContent = msg.content || '';
|
||||
contentHtml = textContent ? `<div class="message-text">${escapeHtml(textContent)}</div>` : '<em>Sin contenido</em>';
|
||||
}
|
||||
}
|
||||
|
||||
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 = '<i class="fas fa-check-double text-primary" title="Leído"></i>';
|
||||
} else if (s === 'delivered') {
|
||||
statusHtml = '<i class="fas fa-check-double text-muted" title="Entregado"></i>';
|
||||
} else if (s === 'sent') {
|
||||
statusHtml = '<i class="fas fa-check text-muted" title="Enviado"></i>';
|
||||
} else {
|
||||
statusHtml = '';
|
||||
}
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="d-flex ${isOutgoing ? 'justify-content-end' : 'justify-content-start'}">
|
||||
<div class="message-bubble ${messageClass}">
|
||||
<div class="message-content">${contentHtml}</div>
|
||||
<div class="message-time">
|
||||
${messageTime} ${isOutgoing ? '<i class="fas fa-check-double text-primary"></i>' : ''}
|
||||
${messageTime} ${statusHtml}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
chatconversations.innerHTML = html;
|
||||
scrollToBottom();
|
||||
@@ -1167,13 +1266,13 @@ if (empty($user_id)) {
|
||||
const messageHtml = `
|
||||
<div class="d-flex ${isOutgoing ? 'justify-content-end' : 'justify-content-start'}">
|
||||
<div class="message-bubble ${messageClass}">
|
||||
<div class="message-content">${escapeHtml(message)}</div>
|
||||
<div class="message-content"><div class="message-text">${escapeHtml(message)}</div></div>
|
||||
<div class="message-time">
|
||||
${now} ${isOutgoing ? '<i class="fas fa-check text-muted"></i>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
|
||||
chatconversations.insertAdjacentHTML('beforeend', messageHtml);
|
||||
scrollToBottom();
|
||||
@@ -1723,7 +1822,7 @@ if (empty($user_id)) {
|
||||
let html = `<div class="message-media">${mediaHtml}</div>`;
|
||||
|
||||
if (message.caption) {
|
||||
html += `<div style="margin-top: 5px;">${escapeHtml(message.caption)}</div>`;
|
||||
html += `<div class="message-text" style="margin-top: 5px;">${escapeHtml(message.caption)}</div>`;
|
||||
}
|
||||
|
||||
if (timestamp) {
|
||||
|
||||
+159
-37
@@ -448,6 +448,15 @@
|
||||
<input type="text" class="form-control border-0" placeholder="Buscar conversaciones..." id="search-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtro de conversaciones: Todos / No leídos -->
|
||||
<div style="padding:8px 12px; display:flex; gap:8px; align-items:center;">
|
||||
<select id="conversation-filter" class="form-select form-select-sm" style="width:auto;">
|
||||
<option value="all">Todos</option>
|
||||
<option value="unread">No leídos</option>
|
||||
</select>
|
||||
<small class="text-muted">Mostrar sólo conversaciones con mensajes no leídos</small>
|
||||
</div>
|
||||
|
||||
<div class="conversation-list" id="conversation-list">
|
||||
<div class="loading">
|
||||
@@ -488,6 +497,7 @@
|
||||
<div id="bot-toggle-container" style="display:flex;align-items:center;gap:6px;margin-right:8px;">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="bot-toggle">Bot: On</button>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="mark-unread-btn" title="Marcar conversación como no leída" style="display:none;">Marcar no leído</button>
|
||||
<button class="btn btn-sm btn-outline-danger" id="delete-conversation-btn" title="Eliminar conversación"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 = `
|
||||
<div class="text-center p-4">
|
||||
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted">No hay conversaciones aún</p>
|
||||
<p class="text-muted">${emptyMsg}</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="loading">
|
||||
@@ -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 = `
|
||||
<div class="text-center p-4">
|
||||
<i class="fas fa-exclamation-triangle text-warning"></i>
|
||||
@@ -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', `<div class="loading loading-top" style="text-align:center; padding:8px; font-size:12px;">Cargando mensajes anteriores...</div>`);
|
||||
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');
|
||||
|
||||
|
||||
+58
-22
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user