mejoras
This commit is contained in:
@@ -116,7 +116,7 @@ try {
|
|||||||
'mime_type' => $msg['mime_type'] ?? null,
|
'mime_type' => $msg['mime_type'] ?? null,
|
||||||
'status' => $msg['status'] ?? 'sent',
|
'status' => $msg['status'] ?? 'sent',
|
||||||
'created_at' => $msg['created_at'],
|
'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']))
|
'date' => date('d/m/Y', strtotime($msg['created_at']))
|
||||||
];
|
];
|
||||||
}, $conversations);
|
}, $conversations);
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ try {
|
|||||||
$offset = ($page - 1) * $limit;
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
// Obtener conversaciones agregadas por usuario: último mensaje + conteo de no leídos + avatar
|
// Obtener conversaciones agregadas por usuario: último mensaje + conteo de no leídos + avatar
|
||||||
|
$filter = isset($_GET['filter']) ? strtolower(trim($_GET['filter'])) : 'all';
|
||||||
|
|
||||||
$sql = "SELECT
|
$sql = "SELECT
|
||||||
u.id AS user_id,
|
u.id AS user_id,
|
||||||
COALESCE(u.name, u.phone_number) AS name,
|
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
|
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
|
) t2 ON t1.user_id = t2.user_id AND t1.created_at = t2.last_time
|
||||||
) lm ON lm.user_id = u.id
|
) lm ON lm.user_id = u.id
|
||||||
GROUP BY u.id
|
GROUP BY u.id";
|
||||||
ORDER BY lm.created_at DESC
|
|
||||||
LIMIT %d OFFSET %d";
|
// 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));
|
$conversations = $db->fetchAll(sprintf($sql, $limit, $offset));
|
||||||
|
|
||||||
// Conteo total de usuarios con al menos una conversación (útil para paginar)
|
// 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;
|
$total = isset($totalRow['count']) ? intval($totalRow['count']) : 0;
|
||||||
|
|
||||||
// Si no hay datos, retornar array vacío
|
// Si no hay datos, retornar array vacío
|
||||||
|
|||||||
+34
-23
@@ -23,11 +23,15 @@ try {
|
|||||||
exit;
|
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();
|
$db = Database::getInstance();
|
||||||
|
|
||||||
// Obtener mensajes del usuario (de ambas tablas para compatibilidad)
|
// Construir consulta: obtener mensajes ordenados DESC (más recientes primero) y limitar
|
||||||
$conversations = $db->fetchAll(
|
$sql = "SELECT
|
||||||
"SELECT
|
|
||||||
id,
|
id,
|
||||||
user_id,
|
user_id,
|
||||||
COALESCE(content, message_text) as content,
|
COALESCE(content, message_text) as content,
|
||||||
@@ -41,25 +45,29 @@ try {
|
|||||||
COALESCE(c.is_read, 0) as is_read
|
COALESCE(c.is_read, 0) as is_read
|
||||||
FROM conversations c
|
FROM conversations c
|
||||||
LEFT JOIN users u ON c.user_id = u.id
|
LEFT JOIN users u ON c.user_id = u.id
|
||||||
WHERE user_id = :user_id
|
WHERE user_id = :user_id";
|
||||||
UNION ALL
|
|
||||||
SELECT
|
$params = ['user_id' => $userId];
|
||||||
id,
|
if ($before) {
|
||||||
user_id,
|
$sql .= " AND created_at < :before";
|
||||||
message_text as content,
|
$params['before'] = $before;
|
||||||
NULL as message_id,
|
}
|
||||||
NULL as media_url,
|
|
||||||
NULL as user_phone,
|
$sql .= " ORDER BY created_at DESC LIMIT " . $limit;
|
||||||
direction,
|
|
||||||
message_type,
|
$conversations = $db->fetchAll($sql, $params);
|
||||||
status,
|
|
||||||
created_at,
|
// Si no hay mensajes, devolver array vacío
|
||||||
1 as is_read
|
if (empty($conversations)) {
|
||||||
FROM conversations
|
echo json_encode(['success' => true, 'data' => [], 'has_more' => false]);
|
||||||
WHERE user_id = :user_id
|
exit;
|
||||||
ORDER BY created_at ASC",
|
}
|
||||||
['user_id' => $userId]
|
|
||||||
);
|
// 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
|
// Si no hay mensajes en conversations, intentar con conversations
|
||||||
if (empty($conversations)) {
|
if (empty($conversations)) {
|
||||||
@@ -116,7 +124,10 @@ try {
|
|||||||
];
|
];
|
||||||
}, $conversations);
|
}, $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) {
|
} catch (Exception $e) {
|
||||||
error_log("Error in get_user_conversations.php: " . $e->getMessage());
|
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']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
+108
-9
@@ -73,12 +73,74 @@ if (empty($user_id)) {
|
|||||||
.message-bubble {
|
.message-bubble {
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
padding: 6px 10px; /* menos padding */
|
padding: 6px 10px; /* menos padding */
|
||||||
border-radius: 12px;
|
border-radius: 18px;
|
||||||
max-width: 70%;
|
max-width: 78%;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
font-size: 0.88rem;
|
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 */
|
/* Estilos para mensajes multimedia */
|
||||||
.message-bubble img {
|
.message-bubble img {
|
||||||
max-width: 220px; /* más pequeño */
|
max-width: 220px; /* más pequeño */
|
||||||
@@ -151,6 +213,13 @@ if (empty($user_id)) {
|
|||||||
text-align: left;
|
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 {
|
.typing-indicator {
|
||||||
display: none;
|
display: none;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -772,9 +841,23 @@ if (empty($user_id)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let html = '';
|
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';
|
const isOutgoing = msg.direction === 'outgoing';
|
||||||
let messageClass = isOutgoing ? 'message-outgoing' : 'message-incoming';
|
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)
|
// Debug: ver todos los datos del mensaje (prioriza media_url_external y valida URLs)
|
||||||
const has_media_external = isValidMediaUrl(msg.media_url_external);
|
const has_media_external = isValidMediaUrl(msg.media_url_external);
|
||||||
@@ -974,29 +1057,45 @@ if (empty($user_id)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (alertInfo) {
|
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
|
// Añadir clase especial a la burbuja
|
||||||
// marca el mensaje como requiring attention mediante messageClass
|
// marca el mensaje como requiring attention mediante messageClass
|
||||||
// we'll append an additional class later
|
// we'll append an additional class later
|
||||||
messageClass += ' message-alert';
|
messageClass += ' message-alert';
|
||||||
} else {
|
} 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 || '');
|
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 += `
|
html += `
|
||||||
<div class="d-flex ${isOutgoing ? 'justify-content-end' : 'justify-content-start'}">
|
<div class="d-flex ${isOutgoing ? 'justify-content-end' : 'justify-content-start'}">
|
||||||
<div class="message-bubble ${messageClass}">
|
<div class="message-bubble ${messageClass}">
|
||||||
<div class="message-content">${contentHtml}</div>
|
<div class="message-content">${contentHtml}</div>
|
||||||
<div class="message-time">
|
<div class="message-time">
|
||||||
${messageTime} ${isOutgoing ? '<i class="fas fa-check-double text-primary"></i>' : ''}
|
${messageTime} ${statusHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
}
|
||||||
|
|
||||||
chatconversations.innerHTML = html;
|
chatconversations.innerHTML = html;
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
@@ -1167,7 +1266,7 @@ if (empty($user_id)) {
|
|||||||
const messageHtml = `
|
const messageHtml = `
|
||||||
<div class="d-flex ${isOutgoing ? 'justify-content-end' : 'justify-content-start'}">
|
<div class="d-flex ${isOutgoing ? 'justify-content-end' : 'justify-content-start'}">
|
||||||
<div class="message-bubble ${messageClass}">
|
<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">
|
<div class="message-time">
|
||||||
${now} ${isOutgoing ? '<i class="fas fa-check text-muted"></i>' : ''}
|
${now} ${isOutgoing ? '<i class="fas fa-check text-muted"></i>' : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -1723,7 +1822,7 @@ if (empty($user_id)) {
|
|||||||
let html = `<div class="message-media">${mediaHtml}</div>`;
|
let html = `<div class="message-media">${mediaHtml}</div>`;
|
||||||
|
|
||||||
if (message.caption) {
|
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) {
|
if (timestamp) {
|
||||||
|
|||||||
+159
-37
@@ -449,6 +449,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="conversation-list" id="conversation-list">
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
@@ -488,6 +497,7 @@
|
|||||||
<div id="bot-toggle-container" style="display:flex;align-items:center;gap:6px;margin-right:8px;">
|
<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>
|
<button class="btn btn-sm btn-outline-secondary" id="bot-toggle">Bot: On</button>
|
||||||
</div>
|
</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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -537,7 +547,14 @@
|
|||||||
this.currentConversationId = null;
|
this.currentConversationId = null;
|
||||||
this.currentUserId = null;
|
this.currentUserId = null;
|
||||||
this.conversations = [];
|
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.conversationsPage = 1;
|
||||||
this.conversationsLimit = 50;
|
this.conversationsLimit = 50;
|
||||||
this.hasMoreConversations = true;
|
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() {
|
setupAutoRefresh() {
|
||||||
@@ -715,7 +743,7 @@
|
|||||||
const loadMoreBtn = document.getElementById('load-more-btn');
|
const loadMoreBtn = document.getElementById('load-more-btn');
|
||||||
if (loadMoreBtn) loadMoreBtn.disabled = true;
|
if (loadMoreBtn) loadMoreBtn.disabled = true;
|
||||||
try {
|
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 resp = await fetch(url);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
console.log('Respuesta get_conversations:', data); // Debug
|
console.log('Respuesta get_conversations:', data); // Debug
|
||||||
@@ -769,10 +797,11 @@
|
|||||||
const container = document.getElementById('conversation-list');
|
const container = document.getElementById('conversation-list');
|
||||||
|
|
||||||
if (this.conversations.length === 0) {
|
if (this.conversations.length === 0) {
|
||||||
|
const emptyMsg = this.conversationFilter === 'unread' ? 'No hay conversaciones no leídas' : 'No hay conversaciones aún';
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="text-center p-4">
|
<div class="text-center p-4">
|
||||||
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -901,10 +930,35 @@
|
|||||||
const btn = document.getElementById('bot-toggle');
|
const btn = document.getElementById('bot-toggle');
|
||||||
const holdIndicator = document.getElementById('hold-indicator');
|
const holdIndicator = document.getElementById('hold-indicator');
|
||||||
const releaseBtn = document.getElementById('release-hold-btn');
|
const releaseBtn = document.getElementById('release-hold-btn');
|
||||||
|
const markUnreadBtn = document.getElementById('mark-unread-btn');
|
||||||
|
|
||||||
if (holdIndicator) {
|
if (markUnreadBtn) {
|
||||||
// Show different labels depending on state
|
markUnreadBtn.style.display = 'inline-block';
|
||||||
if (conv.on_hold) {
|
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.textContent = 'EN ESPERA';
|
||||||
holdIndicator.style.color = '#b85';
|
holdIndicator.style.color = '#b85';
|
||||||
holdIndicator.style.display = 'inline';
|
holdIndicator.style.display = 'inline';
|
||||||
@@ -1051,13 +1105,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargar mensajes
|
// Cargar mensajes (con paginación)
|
||||||
await this.loadconversations(userId);
|
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
|
// Cargar respuestas rápidas
|
||||||
await this.loadQuickReplies();
|
await this.loadQuickReplies();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadconversations(userId, showLoading = true) {
|
async loadconversations(userId, showLoading = true) {
|
||||||
|
// Compatibilidad: ahora delegamos en loadMessages
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
document.getElementById('chat-conversations').innerHTML = `
|
document.getElementById('chat-conversations').innerHTML = `
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
@@ -1067,36 +1134,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Usar apiCall para incluir debug=true y manejo de auth/errores
|
await this.loadMessages(userId, true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading conversations:', error);
|
console.error('Error loading conversations via loadMessages:', error);
|
||||||
document.getElementById('chat-conversations').innerHTML = `
|
document.getElementById('chat-conversations').innerHTML = `
|
||||||
<div class="text-center p-4">
|
<div class="text-center p-4">
|
||||||
<i class="fas fa-exclamation-triangle text-warning"></i>
|
<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() {
|
renderconversations() {
|
||||||
const container = document.getElementById('chat-conversations');
|
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
|
// Si hay un tiempo de expiración y aún no pasó, notificar y retornar
|
||||||
if ($until && $until > $now) {
|
if ($until && $until > $now) {
|
||||||
try {
|
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) {
|
} catch (Exception $e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ class BotService {
|
|||||||
$until = !empty($user['bot_paused_until']) ? strtotime($user['bot_paused_until']) : null;
|
$until = !empty($user['bot_paused_until']) ? strtotime($user['bot_paused_until']) : null;
|
||||||
if ($until && $until > $now) {
|
if ($until && $until > $now) {
|
||||||
try {
|
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) {
|
} catch (Exception $e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -203,7 +203,7 @@ class BotService {
|
|||||||
*/
|
*/
|
||||||
public function sendWelcomeMessage($phoneNumber) {
|
public function sendWelcomeMessage($phoneNumber) {
|
||||||
$enabled = getConfigFromDB('welcome_enabled', '1');
|
$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;
|
if (!$enabled || !$welcomeMessage) return;
|
||||||
|
|
||||||
@@ -238,8 +238,8 @@ class BotService {
|
|||||||
|
|
||||||
case 'asesor':
|
case 'asesor':
|
||||||
case 'ayuda':
|
case 'ayuda':
|
||||||
// Solicitar asesor: marcar solicitud y pausar brevemente (5 minutos)
|
// Solicitar asesor: marcar solicitud y pausar brevemente (3 minutos)
|
||||||
$this->requestAdvisor($phoneNumber, 5);
|
$this->requestAdvisor($phoneNumber, 3);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ class BotService {
|
|||||||
} else {
|
} else {
|
||||||
$this->whatsappService->sendTextMessage(
|
$this->whatsappService->sendTextMessage(
|
||||||
$phoneNumber,
|
$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 .= $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
|
// Actualizar estado del usuario
|
||||||
$this->updateUserMenuState($phoneNumber, $menuId);
|
$this->updateUserMenuState($phoneNumber, $menuId);
|
||||||
@@ -326,17 +331,30 @@ class BotService {
|
|||||||
// DEBUG
|
// DEBUG
|
||||||
try { error_log("[BotService] processMenuSelection - user_id={$user['id']} menu_id={$currentMenuId} received='{$messageText}'"); } catch (Throwable $t) {}
|
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
|
// Verificar si es un número
|
||||||
if (!is_numeric($messageText)) {
|
if (!is_numeric($messageText)) {
|
||||||
$this->whatsappService->sendTextMessage(
|
$this->whatsappService->sendTextMessage(
|
||||||
$phoneNumber,
|
$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}'");
|
error_log("[BotService] processMenuSelection - non-numeric response from user {$user['id']}: '{$messageText}'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$optionNumber = (int)$messageText;
|
$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
|
// Buscar la opción seleccionada por número
|
||||||
$option = $this->db->fetch(
|
$option = $this->db->fetch(
|
||||||
@@ -361,7 +379,7 @@ class BotService {
|
|||||||
if (!$option) {
|
if (!$option) {
|
||||||
$this->whatsappService->sendTextMessage(
|
$this->whatsappService->sendTextMessage(
|
||||||
$phoneNumber,
|
$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}");
|
error_log("[BotService] processMenuSelection - no option found for user {$user['id']} menu={$currentMenuId} option={$optionNumber}");
|
||||||
return;
|
return;
|
||||||
@@ -410,8 +428,8 @@ class BotService {
|
|||||||
$message = $option['action_value'] ?: "Gracias por usar nuestro servicio. ¡Hasta pronto!";
|
$message = $option['action_value'] ?: "Gracias por usar nuestro servicio. ¡Hasta pronto!";
|
||||||
$this->whatsappService->sendTextMessage($phoneNumber, $message);
|
$this->whatsappService->sendTextMessage($phoneNumber, $message);
|
||||||
$this->exitMenu($phoneNumber);
|
$this->exitMenu($phoneNumber);
|
||||||
// Pausar el bot para esta conversación por 5 minutos (antes eran 4 horas)
|
// Pausar el bot para esta conversación por 3 minutos (antes eran 4 horas)
|
||||||
$this->pauseConversationForMinutes($phoneNumber, 5);
|
$this->pauseConversationForMinutes($phoneNumber, 3);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,7 +442,7 @@ class BotService {
|
|||||||
// Por ejemplo, consultar saldos, procesar pagos, etc.
|
// Por ejemplo, consultar saldos, procesar pagos, etc.
|
||||||
$this->whatsappService->sendTextMessage(
|
$this->whatsappService->sendTextMessage(
|
||||||
$phoneNumber,
|
$phoneNumber,
|
||||||
"🔄 Procesando tu solicitud... Un momento por favor."
|
"🔄 Procesando su solicitud... Un momento, por favor."
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simular procesamiento
|
// Simular procesamiento
|
||||||
@@ -432,7 +450,7 @@ class BotService {
|
|||||||
|
|
||||||
$this->whatsappService->sendTextMessage(
|
$this->whatsappService->sendTextMessage(
|
||||||
$phoneNumber,
|
$phoneNumber,
|
||||||
"✅ Tu solicitud ha sido procesada exitosamente."
|
"✅ Su solicitud ha sido procesada con éxito."
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->exitMenu($phoneNumber);
|
$this->exitMenu($phoneNumber);
|
||||||
@@ -467,7 +485,7 @@ class BotService {
|
|||||||
* Enviar mensaje por defecto cuando no hay coincidencias
|
* Enviar mensaje por defecto cuando no hay coincidencias
|
||||||
*/
|
*/
|
||||||
private function sendDefaultNoMatch($phoneNumber) {
|
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);
|
$this->whatsappService->sendTextMessage($phoneNumber, $defaultMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +504,7 @@ class BotService {
|
|||||||
/**
|
/**
|
||||||
* Poner la conversación en pausa por X minutos (llamada desde opciones 'end')
|
* 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 {
|
try {
|
||||||
$pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes"));
|
$pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes"));
|
||||||
$this->db->update('users', ['bot_paused_until' => $pausedUntil], 'phone_number = :phone', ['phone' => $phoneNumber]);
|
$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
|
* Poner conversación en espera (asesor solicitado) por X minutos
|
||||||
*/
|
*/
|
||||||
public function putOnHold($phoneNumber, $minutes = 5) {
|
public function putOnHold($phoneNumber, $minutes = 3) {
|
||||||
try {
|
try {
|
||||||
$pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes"));
|
$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)
|
// 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]);
|
$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
|
// 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");
|
if (function_exists('writeLog')) writeLog('INFO', "Advisor requested for $phoneNumber until $pausedUntil");
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('putOnHold failed: ' . $e->getMessage());
|
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)
|
* 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 {
|
try {
|
||||||
$pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes"));
|
$pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes"));
|
||||||
// No ponemos on_hold para evitar bloqueo automático; usamos advisor_requested
|
// 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->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");
|
if (function_exists('writeLog')) writeLog('INFO', "Advisor solicited for $phoneNumber until $pausedUntil");
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
@@ -537,7 +555,7 @@ class BotService {
|
|||||||
public function releaseHold($phoneNumber) {
|
public function releaseHold($phoneNumber) {
|
||||||
try {
|
try {
|
||||||
$this->db->update('users', ['on_hold' => 0, 'bot_paused_until' => null, 'advisor_requested' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
|
$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");
|
if (function_exists('writeLog')) writeLog('INFO', "Advisor released hold for $phoneNumber");
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('releaseHold failed: ' . $e->getMessage());
|
error_log('releaseHold failed: ' . $e->getMessage());
|
||||||
@@ -556,8 +574,7 @@ class BotService {
|
|||||||
|
|
||||||
// Notificar al usuario
|
// Notificar al usuario
|
||||||
try {
|
try {
|
||||||
$this->whatsappService->sendTextMessage($phoneNumber, "🔔 Un asesor está atendiendo tu conversación. Por favor, espera su respuesta.");
|
$this->whatsappService->sendTextMessage($phoneNumber, "🔔 Un asesor está atendiendo su conversación. Por favor, espere su respuesta.");
|
||||||
} catch (Exception $e) {
|
|
||||||
// ignore send errors
|
// 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
|
* Actualizar estado del menú del usuario
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class BotServiceLab
|
|||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log("Error en BotServiceLab->processMessage: " . $e->getMessage());
|
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) {
|
if (!$response) {
|
||||||
// No se encontró respuesta, enviar mensaje por defecto
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,13 +180,13 @@ class BotServiceLab
|
|||||||
if ($messageType === 'image' || $messageType === 'document') {
|
if ($messageType === 'image' || $messageType === 'document') {
|
||||||
$this->handleUploadedDocument($user, $mediaId, $currentState['state']);
|
$this->handleUploadedDocument($user, $mediaId, $currentState['state']);
|
||||||
} else {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si no está esperando media, informar
|
// 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);
|
$mediaUrl = $this->whatsappService->downloadMedia($mediaId);
|
||||||
|
|
||||||
if (!$mediaUrl) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ class BotServiceLab
|
|||||||
'order_received_at' => date('Y-m-d H:i:s')
|
'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);
|
$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')
|
'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
|
// Aquí iría lógica adicional según el flujo
|
||||||
$this->stateService->setState($phoneNumber, ConversationStateService::STATE_VALIDATING_DATA);
|
$this->stateService->setState($phoneNumber, ConversationStateService::STATE_VALIDATING_DATA);
|
||||||
@@ -233,7 +233,7 @@ class BotServiceLab
|
|||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log("Error downloading media: " . $e->getMessage());
|
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);
|
$result = $this->whatsappService->sendTemplateMessage($phoneNumber, $templateName, $parameters);
|
||||||
|
|
||||||
if (!$result) {
|
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) {
|
} catch (Exception $e) {
|
||||||
error_log("Error sending template: " . $e->getMessage());
|
error_log("Error sending template: " . $e->getMessage());
|
||||||
|
|||||||
Reference in New Issue
Block a user