From 2017c8c8056851a08c212768c4eccec548e61b01 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:12:27 -0500 Subject: [PATCH] mejoras --- api/get_conversation_detail.php | 2 +- api/get_conversations.php | 19 ++- api/get_user_messages.php | 63 ++++++---- api/mark_conversation_unread.php | 31 +++++ chat_window.php | 121 +++++++++++++++++-- conversations.php | 196 +++++++++++++++++++++++++------ services/BotService.php | 80 +++++++++---- services/BotServiceLab.php | 18 +-- 8 files changed, 420 insertions(+), 110 deletions(-) create mode 100644 api/mark_conversation_unread.php diff --git a/api/get_conversation_detail.php b/api/get_conversation_detail.php index 619cf16..3eb0d27 100644 --- a/api/get_conversation_detail.php +++ b/api/get_conversation_detail.php @@ -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); diff --git a/api/get_conversations.php b/api/get_conversations.php index fe51f5a..d6807fb 100644 --- a/api/get_conversations.php +++ b/api/get_conversations.php @@ -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 diff --git a/api/get_user_messages.php b/api/get_user_messages.php index 9b96216..fa64ff2 100644 --- a/api/get_user_messages.php +++ b/api/get_user_messages.php @@ -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()); diff --git a/api/mark_conversation_unread.php b/api/mark_conversation_unread.php new file mode 100644 index 0000000..e28122a --- /dev/null +++ b/api/mark_conversation_unread.php @@ -0,0 +1,31 @@ + 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']); +} +?> \ No newline at end of file diff --git a/chat_window.php b/chat_window.php index 864bd82..326d01d 100644 --- a/chat_window.php +++ b/chat_window.php @@ -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 = `
No hay conversaciones aún
+${emptyMsg}