Files
whatsapp/conversations.php
T

1904 lines
84 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>💬 Conversaciones - WhatsApp Bot</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--whatsapp-green: #25d366;
--whatsapp-green-dark: #128c7e;
--whatsapp-light: #dcf8c6;
--whatsapp-dark: #075e54;
--message-bg: #f0f0f0;
--sidebar-bg: #f8f9fa;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
background: #e5ddd5;
margin: 0;
height: 100vh;
overflow: hidden;
}
.chat-container {
display: flex;
height: 100vh;
background: white;
}
.chat-sidebar {
width: 350px;
background: var(--sidebar-bg);
border-right: 1px solid #e1e5e9;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 20px;
background: var(--whatsapp-dark);
color: white;
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-search {
padding: 10px 20px;
background: white;
border-bottom: 1px solid #e1e5e9;
}
.conversation-list {
flex: 1;
overflow-y: auto;
}
.conversation-item {
padding: 15px 20px;
border-bottom: 1px solid #f1f1f1;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
}
.conversation-item:hover {
background: #f5f5f5;
}
.conversation-item.active {
background: #e3f2fd;
border-right: 3px solid var(--whatsapp-green);
}
.conversation-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--whatsapp-green);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 15px;
flex-shrink: 0;
}
.conversation-info {
flex: 1;
min-width: 0;
}
.conversation-name {
font-weight: 600;
font-size: 16px;
color: #333;
margin-bottom: 5px;
}
.conversation-preview {
color: #666;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-time {
color: #999;
font-size: 12px;
text-align: right;
flex-shrink: 0;
margin-left: 10px;
}
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
background: #e5ddd5;
}
.chat-header {
padding: 15px 20px;
background: var(--whatsapp-dark);
color: white;
border-bottom: 1px solid #ddd;
display: flex;
align-items: center;
}
.chat-header-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--whatsapp-green);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 15px;
}
.chat-header-info h6 {
margin: 0;
font-size: 16px;
}
.chat-header-info small {
color: #ccc;
}
.chat-conversations {
flex: 1;
overflow-y: auto;
padding: 20px;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGRlZnM+CjxwYXR0ZXJuIGlkPSJ3aGF0c2FwcC1iZyIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPgo8cG9seWdvbiBmaWxsPSJyZ2JhKDIyOSwyMjEsMjEzLDAuMSkiIHBvaW50cz0iMCwwIDEwMCwwIDEwMCwxMDAgMCwxMDAiLz4KPGV+';
}
.message {
max-width: 70%;
margin-bottom: 10px;
display: flex;
}
.message.outgoing {
justify-content: flex-end;
}
.message.incoming {
justify-content: flex-start;
}
.message-bubble {
padding: 8px 12px;
border-radius: 18px;
position: relative;
word-wrap: break-word;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
}
.message.outgoing .message-bubble {
background: var(--whatsapp-light);
color: #333;
border-bottom-right-radius: 4px;
}
.message.incoming .message-bubble {
background: white;
color: #333;
border-bottom-left-radius: 4px;
}
.message-time {
font-size: 11px;
color: #999;
text-align: right;
margin-top: 5px;
}
.message-status {
display: inline-block;
margin-left: 5px;
font-size: 12px;
}
.status-sent { color: #999; }
.status-delivered { color: #999; }
.status-read { color: var(--whatsapp-green); }
.chat-input {
padding: 15px 20px;
background: #f0f0f0;
border-top: 1px solid #ddd;
display: flex;
align-items: center;
gap: 10px;
}
.chat-input input[type="text"] {
flex: 1;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 25px;
outline: none;
font-size: 14px;
}
.chat-input input:focus {
border-color: var(--whatsapp-green);
}
.chat-input .btn {
border-radius: 50%;
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
background: var(--whatsapp-green);
border: none;
color: white;
}
.chat-input .btn:hover {
background: var(--whatsapp-green-dark);
}
#attach-btn {
background: #075E54;
}
#attach-btn:hover {
background: #128C7E;
}
/* Estilos para mensajes multimedia */
.message-media {
max-width: 300px;
border-radius: 8px;
overflow: hidden;
margin-bottom: 5px;
}
.message-media img,
.message-media video {
width: 100%;
display: block;
cursor: pointer;
}
.message-media audio {
width: 100%;
}
.message-document {
background: rgba(0,0,0,0.05);
padding: 10px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.message-document:hover {
background: rgba(0,0,0,0.1);
}
.message-document i {
font-size: 24px;
color: var(--whatsapp-green);
}
.document-info {
flex: 1;
}
.document-name {
font-weight: 500;
font-size: 14px;
}
.document-size {
font-size: 12px;
color: #666;
}
#media-preview {
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.upload-progress {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--whatsapp-green);
transform-origin: left;
transition: transform 0.3s;
}
.no-conversation {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
text-align: center;
}
.no-conversation i {
font-size: 64px;
margin-bottom: 20px;
opacity: 0.3;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
/* Notification toasts (compact by default) */
#notification-toasts {
position: fixed;
top: 12px;
right: 12px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-end;
pointer-events: none;
}
.notification-toast {
pointer-events: auto;
background: #fff;
color: #222;
padding: 8px 10px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
display: flex;
align-items: center;
gap: 10px;
min-width: 160px;
max-width: 260px;
font-size: 13px;
}
.notification-toast.small {
padding: 6px 8px;
font-size: 12px;
min-width: 140px;
max-width: 220px;
}
.notification-toast.cool {
background: linear-gradient(90deg,#6a11cb,#2575fc);
color: white;
box-shadow: 0 6px 16px rgba(37,117,252,0.28);
transform-origin: right;
animation: pop 320ms ease;
}
@keyframes pop { from { transform: translateY(-6px) scale(0.98); opacity:0 } to { transform: translateY(0) scale(1); opacity:1 } }
.notification-toast .nt-icon { font-size: 16px; margin-right: 6px; opacity: 0.95; }
.notification-toast .nt-actions { margin-left: auto; display:flex; gap:6px; }
.notification-toast .btn { font-size: 12px; padding: 4px 8px; }
.notification-toast.urgent { border-left: 4px solid #e74c3c; }
.message-template {
background: #fffacd;
border: 1px solid #f0e68c;
padding: 10px;
border-radius: 8px;
margin-bottom: 10px;
font-size: 13px;
}
.unread-count {
background: var(--whatsapp-green);
color: white;
border-radius: 50%;
font-size: 12px;
min-width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
}
@media (max-width: 768px) {
.chat-sidebar {
width: 100%;
position: absolute;
z-index: 1000;
height: 100vh;
}
.chat-main {
width: 100%;
}
.chat-sidebar.show-chat {
transform: translateX(-100%);
}
}
.reply-preview {
background: #f4f6f8;
border-left: 3px solid #d0d7de;
padding: 6px 8px;
margin-bottom: 6px;
font-size: 12px;
color: #555;
border-radius: 4px;
}
.reaction-badge {
display: inline-block;
background: rgba(0,0,0,0.06);
padding: 2px 6px;
border-radius: 12px;
font-size: 12px;
margin-top: 8px;
}
.conversation-avatar img, .conversation-avatar-img { max-width: 44px; max-height:44px; border-radius:50%; }
.conversation-avatar-wrapper { width:48px; display:flex; align-items:center; justify-content:center }
.conversation-item.unread { background: rgba(255, 248, 220, 0.9); }
.unread-badge { margin-left: 8px; font-size: 12px; }
</style>
</head>
<body>
<div class="chat-container">
<!-- Sidebar con lista de conversaciones -->
<div class="chat-sidebar">
<div class="sidebar-header">
<div>
<h5 class="mb-0">💬 Conversaciones</h5>
</div>
<div>
<a href="index.php" class="text-white text-decoration-none">
<i class="fas fa-arrow-left"></i>
</a>
</div>
</div>
<div class="sidebar-search">
<div class="input-group">
<span class="input-group-text bg-transparent border-0">
<i class="fas fa-search text-muted"></i>
</span>
<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">
<i class="fas fa-spinner fa-spin"></i>
<div>Cargando conversaciones...</div>
</div>
</div>
<div id="load-more-container" style="padding:10px; text-align:center; display:none;">
<button id="load-more-btn" class="btn btn-sm btn-outline-primary">Cargar más</button>
</div>
</div>
<!-- Área principal del chat -->
<div class="chat-main">
<div id="no-conversation" class="no-conversation">
<i class="fab fa-whatsapp"></i>
<h4>WhatsApp Bot Manager</h4>
<p>Selecciona una conversación para comenzar a chatear</p>
</div>
<div id="chat-area" style="display: none; height: 100%; flex-direction: column;">
<div class="chat-header">
<div class="chat-header-avatar" id="chat-avatar">
<i class="fas fa-user"></i>
</div>
<div class="chat-header-info">
<h6 id="chat-name">Usuario <button class="btn btn-sm btn-link" id="edit-user-btn" title="Editar usuario"><i class="fas fa-user-edit"></i></button>
<span id="hold-indicator" style="display:none; margin-left:8px; color:#b85; font-weight:600">EN ESPERA</span>
</h6>
<small id="chat-phone">+1234567890</small>
<div style="font-size:12px; margin-top:4px;">
<button id="attend-btn" class="btn btn-sm btn-outline-success" style="display:none; margin-right:6px;">Atender</button>
<button id="finish-attend-btn" class="btn btn-sm btn-outline-warning" style="display:none; margin-right:6px;">Finalizar atención</button>
<button id="release-hold-btn" class="btn btn-sm btn-outline-primary" style="display:none">Liberar espera</button>
</div>
</div>
<div style="margin-left: auto; display:flex; gap:8px; align-items:center;">
<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>
<div class="chat-conversations" id="chat-conversations">
<!-- Los mensajes se cargan aquí -->
</div>
<!-- Preview de archivo multimedia -->
<div id="media-preview" style="display: none; padding: 10px; background: #f0f0f0; border-top: 1px solid #ddd;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 10px;">
<img id="preview-thumbnail" style="max-width: 60px; max-height: 60px; border-radius: 5px; display: none;">
<div>
<div id="preview-filename" style="font-weight: bold; font-size: 14px;"></div>
<div id="preview-filesize" style="font-size: 12px; color: #666;"></div>
</div>
</div>
<button class="btn btn-sm btn-danger" onclick="cancelMediaUpload()">
<i class="fas fa-times"></i>
</button>
</div>
<input type="text" id="media-caption" class="form-control mt-2" placeholder="Agregar un comentario (opcional)" maxlength="1024">
</div>
<div class="chat-input">
<input type="file" id="file-input" accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx" style="display: none;">
<button class="btn" id="attach-btn" title="Adjuntar archivo">
<i class="fas fa-paperclip"></i>
</button>
<div class="quick-replies" id="quick-replies" style="display:flex;gap:6px;align-items:center;">
<!-- Quick replies buttons inserted dynamically -->
</div>
<input type="text" placeholder="Escribe un mensaje..." id="message-input" maxlength="4096">
<button class="btn" id="send-btn">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script>
class WhatsAppChat {
constructor() {
this.currentConversationId = null;
this.currentUserId = null;
this.conversations = [];
// 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;
this.loadingConversations = false;
this.init();
}
init() {
this.loadConversations();
this.setupEventListeners();
this.setupAutoRefresh();
this.setupNotificationPolling();
}
setupNotificationPolling() {
// Poll every 7 seconds for unread notifications
this.loadNotifications();
setInterval(() => this.loadNotifications(), 7000);
}
async loadNotifications() {
try {
const resp = await fetch('api/get_notifications.php', { credentials: 'same-origin' });
if (resp.status === 401) {
console.warn('Notifications fetch: unauthorized (session may have expired)');
return;
}
const json = await resp.json();
console.debug('loadNotifications response:', json);
if (json && json.success && Array.isArray(json.data)) {
json.data.forEach(n => this.showNotificationToast(n));
} else if (json && json.success === false) {
console.warn('get_notifications returned error:', json.error || json);
}
} catch (e) {
console.error('Error loading notifications', e);
}
}
async showNotificationToast(notification) {
// Create or reuse container
const containerId = 'notification-toasts';
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
container.id = containerId;
document.body.appendChild(container);
}
const toast = document.createElement('div');
// compact by default
toast.className = 'notification-toast small';
if (notification.cool || notification.type === 'cool') toast.classList.add('cool');
if (notification.level === 'urgent' || notification.urgent) toast.classList.add('urgent');
const iconSpan = document.createElement('span');
iconSpan.className = 'nt-icon';
iconSpan.textContent = notification.icon || (notification.cool ? '✨' : (notification.level === 'urgent' ? '⚠️' : '🔔'));
toast.appendChild(iconSpan);
const msg = document.createElement('div');
msg.style.flex = '1';
msg.style.fontSize = '13px';
msg.style.lineHeight = '1.2';
msg.textContent = notification.message || 'Nuevo evento';
toast.appendChild(msg);
const actions = document.createElement('div');
actions.className = 'nt-actions';
const openBtn = document.createElement('button');
openBtn.className = notification.cool ? 'btn btn-sm btn-light' : 'btn btn-sm btn-primary';
openBtn.textContent = 'Abrir';
openBtn.onclick = async () => {
await fetch('api/mark_notification_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id: notification.id})});
let data = {};
try { data = notification.data ? JSON.parse(notification.data) : {}; } catch(e) {}
const userId = data.user_id || notification.user_id;
if (userId) {
const conv = this.conversations.find(c => c.user_id == userId);
if (conv) {
this.openConversation(conv.user_id, conv.name, conv.phone_number);
} else {
this.openConversation(userId, 'Usuario', '');
}
}
toast.remove();
};
const dismiss = document.createElement('button');
dismiss.className = 'btn btn-sm btn-outline-secondary';
dismiss.textContent = '×';
dismiss.title = 'Descartar';
dismiss.onclick = async () => {
await fetch('api/mark_notification_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id: notification.id})});
toast.remove();
};
actions.appendChild(openBtn);
actions.appendChild(dismiss);
toast.appendChild(actions);
container.appendChild(toast);
// Auto remove (shorter for compact notifications)
const timeout = notification.cool ? 15000 : 8000;
setTimeout(() => { if (toast.parentNode) toast.remove(); }, timeout);
}
setupEventListeners() {
// Búsqueda de conversaciones
document.getElementById('search-input').addEventListener('input', (e) => {
this.searchConversations(e.target.value);
});
// Envío de mensajes
document.getElementById('send-btn').addEventListener('click', () => {
this.sendMessage();
});
document.getElementById('message-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
// Adjuntar archivos
document.getElementById('attach-btn').addEventListener('click', () => {
document.getElementById('file-input').click();
});
document.getElementById('file-input').addEventListener('change', (e) => {
this.handleFileSelect(e);
});
// Carga paginada: botón "Cargar más" y scroll infinito
const loadMoreBtn = document.getElementById('load-more-btn');
if (loadMoreBtn) {
loadMoreBtn.addEventListener('click', () => this.loadMoreConversations());
}
const convList = document.getElementById('conversation-list');
if (convList) {
convList.addEventListener('scroll', () => {
if (this.hasMoreConversations && !this.loadingConversations && (convList.scrollTop + convList.clientHeight >= convList.scrollHeight - 60)) {
this.loadMoreConversations();
}
});
}
// 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() {
// Actualizar conversaciones cada 30 segundos
setInterval(() => {
this.loadConversations();
}, 30000);
// Actualizar mensajes del chat activo cada 10 segundos
setInterval(() => {
if (this.currentUserId) {
this.loadconversations(this.currentUserId, false);
}
}, 10000);
}
// Helper para llamadas a la API desde esta clase
async apiCall(endpoint, options = {}) {
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
};
// Si se pasa body, asumimos POST (y serializamos)
const finalOptions = { ...defaultOptions, ...options };
if (options.body && typeof options.body === 'object') {
finalOptions.method = 'POST';
finalOptions.body = JSON.stringify(options.body);
}
// Normalizar endpoint: prefijar 'api/' si no es URL completa y no empieza con 'api/'
let url = endpoint;
if (!/^https?:\/\//i.test(url) && !url.startsWith('api/')) {
url = 'api/' + url;
}
const response = await fetch(url, finalOptions);
if (response.status === 401) {
// Sesión expirada: redirigir al login
console.warn('apiCall: unauthorized, redirecting to login');
window.location.href = 'login.php';
return null;
}
const text = await response.text();
if (!response.ok) {
// Incluir el cuerpo de la respuesta (truncado) para diagnóstico
const snippet = text && text.length ? (text.length > 2000 ? text.substr(0, 2000) + '... (truncated)' : text) : '<no body>';
console.error('apiCall HTTP error', response.status, url, snippet);
throw new Error('HTTP error! status: ' + response.status + ' -- ' + snippet);
}
if (!text || text.trim() === '') return null;
try {
return JSON.parse(text);
} catch (err) {
console.error('apiCall parse error for', url, err);
const snippet = text.length > 1000 ? text.substr(0, 1000) : text;
throw new Error('Invalid JSON response from ' + url + ': ' + err.message + ' -- response snippet: ' + snippet);
}
}
async loadConversations(page = 1, append = false) {
if (this.loadingConversations) return;
this.loadingConversations = true;
const loadMoreContainer = document.getElementById('load-more-container');
const loadMoreBtn = document.getElementById('load-more-btn');
if (loadMoreBtn) loadMoreBtn.disabled = true;
try {
const 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
let items = [];
let hasMore = false;
if (data && data.success && Array.isArray(data.data)) {
items = data.data;
hasMore = !!data.has_more || (page * this.conversationsLimit) < (data.total || 0);
} else if (Array.isArray(data)) {
items = data;
hasMore = items.length === this.conversationsLimit;
} else {
console.error('Error cargando conversaciones: formato de datos inválido', data);
alert('Error cargando conversaciones: ' + (data.error || 'formato de datos inválido'));
this.loadingConversations = false;
if (loadMoreBtn) loadMoreBtn.disabled = false;
return;
}
if (append) {
this.conversations = this.conversations.concat(items);
} else {
this.conversations = items;
}
this.hasMoreConversations = hasMore;
this.conversationsPage = page;
this.renderConversations();
if (loadMoreContainer) {
loadMoreContainer.style.display = this.hasMoreConversations ? 'block' : 'none';
}
} catch (error) {
console.error('Error loading conversations:', error);
alert('Error cargando conversaciones: ' + error.message);
} finally {
this.loadingConversations = false;
if (loadMoreBtn) loadMoreBtn.disabled = false;
}
}
async loadMoreConversations() {
if (!this.hasMoreConversations || this.loadingConversations) return;
await this.loadConversations(this.conversationsPage + 1, true);
}
renderConversations() {
const container = document.getElementById('conversation-list');
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">${emptyMsg}</p>
</div>
`;
return;
}
// Ahora el backend devuelve por usuario: last_message, last_time, unread_count y avatar_url
const html = this.conversations.map(conv => {
const isActive = conv.user_id == this.currentUserId ? 'active' : '';
const time = this.formatTime(conv.last_time);
const preview = this.truncateText(conv.last_message || 'Sin mensajes', 50);
const avatar = conv.avatar_url ? `<img src="${conv.avatar_url}" alt="avatar" class="conversation-avatar-img">` : `<div class="conversation-avatar">${this.getInitials(conv.name)}</div>`;
const unreadBadge = conv.unread_count && conv.unread_count > 0 ? `<span class="badge bg-danger unread-badge">${conv.unread_count}</span>` : '';
const attentionBadge = conv.advisor_requested ? `<span class="badge bg-danger ms-1">¡Atención!</span>` : '';
// different color if has unread and not active
const unreadClass = (conv.unread_count && conv.unread_count > 0 && !isActive) ? 'unread' : '';
const attentionClass = conv.advisor_requested ? ' attention' : '';
return `
<div class="conversation-item ${isActive} ${unreadClass}${attentionClass}" data-user-id="${conv.user_id}" onclick="chat.openConversation(${conv.user_id}, '${conv.name}', '${conv.phone_number}')">
<div class="conversation-avatar-wrapper">
${avatar}
</div>
<div class="conversation-info">
<div class="conversation-name">${conv.name} ${unreadBadge} ${attentionBadge}</div>
<div class="conversation-preview">
${conv.direction === 'outgoing' ? '✓ ' : ''}${preview}
</div>
</div>
<div class="conversation-time">
${time}
</div>
</div>
`;
}).join('');
// Detectar nuevas notificaciones: comparar unread counts previos
if (!this._prevConversations) this._prevConversations = {};
this.conversations.forEach(c => {
const prev = this._prevConversations[c.user_id] || { unread_count: 0 };
if (c.unread_count > prev.unread_count && c.user_id != this.currentUserId) {
// nueva notificación
this.playNotificationSound();
}
this._prevConversations[c.user_id] = { unread_count: c.unread_count };
});
container.innerHTML = html;
}
getInitials(name) {
if (!name) return '?';
const parts = name.split(' ');
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase();
}
return name.substring(0, 2).toUpperCase();
}
formatTime(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffInHours = (now - date) / (1000 * 60 * 60);
if (diffInHours < 24) {
return date.toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
});
} else if (diffInHours < 48) {
return 'Ayer';
} else {
return date.toLocaleDateString('es-ES', {
day: '2-digit',
month: '2-digit'
});
}
}
truncateText(text, maxLength) {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
playNotificationSound() {
try {
// Small beep using Web Audio API (no external file needed)
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.value = 880;
o.connect(g);
g.connect(ctx.destination);
g.gain.value = 0.05;
o.start();
setTimeout(() => { o.stop(); ctx.close(); }, 180);
} catch (e) {
console.warn('Notification sound failed', e);
}
}
async openConversation(userId, userName, phoneNumber) {
this.currentUserId = userId;
// Actualizar UI
document.getElementById('no-conversation').style.display = 'none';
document.getElementById('chat-area').style.display = 'flex';
// Actualizar header
document.getElementById('chat-name').textContent = userName;
document.getElementById('chat-phone').textContent = phoneNumber;
document.getElementById('chat-avatar').textContent = this.getInitials(userName);
// Marcar conversación como activa
document.querySelectorAll('.conversation-item').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`[data-user-id="${userId}"]`)?.classList.add('active');
// Actualizar estado del toggle del bot según datos de la conversación
const conv = this.conversations.find(c => c.user_id === userId);
if (conv) {
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 (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');
}
};
}
if (holdIndicator) {
if (conv.on_hold) {
holdIndicator.textContent = 'EN ESPERA';
holdIndicator.style.color = '#b85';
holdIndicator.style.display = 'inline';
} else if (conv.advisor_requested) {
holdIndicator.textContent = 'SOLICITUD PENDIENTE';
holdIndicator.style.color = '#f39c12';
holdIndicator.style.display = 'inline';
} else {
holdIndicator.style.display = 'none';
}
} else {
console.warn('holdIndicator element not found in DOM');
}
if (releaseBtn) {
// show button when on_hold or advisor_requested
releaseBtn.style.display = (conv.on_hold || conv.advisor_requested) ? 'inline-block' : 'none';
releaseBtn.onclick = async () => {
try {
const resp = await fetch('api/release_hold.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) {
conv.on_hold = false;
conv.advisor_requested = 0;
if (holdIndicator) {
holdIndicator.style.display = 'none';
} else {
console.warn('holdIndicator missing when releasing hold');
}
releaseBtn.style.display = 'none';
await this.loadConversations();
} else {
alert('Error al liberar la espera');
}
} catch (e) {
console.error('Error releasing hold', e);
alert('Error liberando espera');
}
};
// Attender (marcar 'en servicio' sin liberar)
const attendBtn = document.getElementById('attend-btn');
const finishBtn = document.getElementById('finish-attend-btn');
if (attendBtn) {
attendBtn.style.display = (conv.advisor_requested && !conv.in_service) ? 'inline-block' : 'none';
attendBtn.onclick = async () => {
try {
const resp = await fetch('api/attend.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) {
conv.in_service = true;
conv.advisor_requested = 0;
if (holdIndicator) {
holdIndicator.textContent = 'EN SERVICIO';
holdIndicator.style.color = '#28a745';
holdIndicator.style.display = 'inline';
} else {
console.warn('holdIndicator missing when marking in_service');
}
attendBtn.style.display = 'none';
// keep release available
releaseBtn.style.display = 'inline-block';
// show finish button
if (finishBtn) finishBtn.style.display = 'inline-block';
await this.loadConversations();
} else {
alert('Error al marcar como atendida');
}
} catch (e) {
console.error('Error attending', e);
alert('Error al atender');
}
};
}
// Finalizar atención: enviar encuesta y limpiar estado
if (finishBtn) {
finishBtn.style.display = conv.in_service ? 'inline-block' : 'none';
finishBtn.onclick = async () => {
if (!confirm('¿Finalizar atención y enviar encuesta al usuario?')) return;
try {
const resp = await fetch('api/finish_attend.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) {
conv.in_service = false;
conv.advisor_requested = 0;
holdIndicator.style.display = 'none';
finishBtn.style.display = 'none';
releaseBtn.style.display = 'none';
attendBtn.style.display = 'none';
await this.loadConversations();
} else {
alert('Error al finalizar la atención');
}
} catch (e) {
console.error('Error finalizing attend', e);
alert('Error al finalizar la atención');
}
};
}
}
if (btn) {
btn.textContent = conv.bot_enabled ? 'Bot: On' : 'Bot: Off';
btn.classList.toggle('btn-outline-danger', !conv.bot_enabled);
btn.classList.toggle('btn-outline-secondary', conv.bot_enabled);
btn.onclick = async () => {
try {
const resp = await fetch('api/set_bot_enabled.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: userId, enabled: conv.bot_enabled ? 0 : 1 })
});
const json = await resp.json();
if (json && json.success) {
conv.bot_enabled = !conv.bot_enabled;
btn.textContent = conv.bot_enabled ? 'Bot: On' : 'Bot: Off';
btn.classList.toggle('btn-outline-danger', !conv.bot_enabled);
btn.classList.toggle('btn-outline-secondary', conv.bot_enabled);
} else {
alert('Error al cambiar el estado del bot');
}
} catch (e) {
console.error('Error toggling bot', e);
}
};
}
}
// 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">
<i class="fas fa-spinner fa-spin"></i> Cargando mensajes...
</div>
`;
}
try {
await this.loadMessages(userId, true);
} catch (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>
<p>Error al cargar los mensajes</p>
</div>
`;
}
}
/**
* 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_messages.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);
if (container) {
container.innerHTML = `
<div class="text-center p-4">
<i class="fas fa-exclamation-triangle text-warning fa-2x"></i>
<p class="mt-3">Error al cargar los mensajes: ${error.message || error}</p>
<button class="btn btn-sm btn-primary" id="retry-load-messages">Reintentar</button>
</div>
`;
const retryBtn = document.getElementById('retry-load-messages');
if (retryBtn) retryBtn.addEventListener('click', async () => { await this.loadMessages(userId, true); });
}
} finally {
this.loadingMessages = false;
}
}
renderconversations() {
const container = document.getElementById('chat-conversations');
if (this.conversations.length === 0) {
container.innerHTML = `
<div class="text-center p-4">
<i class="far fa-comment fa-3x text-muted mb-3"></i>
<p class="text-muted">No hay mensajes en esta conversación</p>
</div>
`;
return;
}
const html = this.conversations.map(msg => {
const time = new Date(msg.created_at).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
});
const statusIcon = this.getStatusIcon(msg.status);
// Renderizar contenido (texto o multimedia)
// Preferir miniatura local para previsualización si existe
const content = (msg.local_thumb || msg.local_file || msg.media_url_external)
? this.renderMediaMessage(msg)
: (msg.content || msg.message_text || '[Mensaje vacío]');
// Mostrar preview si es reply/context
let replyHtml = '';
if (msg.reply_to_message_id) {
const target = this.conversations.find(m => m.message_id == msg.reply_to_message_id);
const previewText = target ? (target.content || target.message_text || '').substring(0,140) : ('Mensaje ' + msg.reply_to_message_id);
replyHtml = `<div class="reply-preview">En respuesta a: ${previewText}</div>`;
}
// Mostrar reacción si existe
let reactionHtml = '';
if (msg.reaction_emoji) {
reactionHtml = `<div class="reaction-badge">${msg.reaction_emoji}</div>`;
}
return `
<div class="message ${msg.direction}">
<div class="message-bubble">
${replyHtml}
${content}
${reactionHtml}
<div class="message-actions mt-1">
<button class="btn btn-sm btn-link" onclick="app.replyToMessage('${msg.message_id}','${msg.user_phone}')" title="Responder"><i class="fas fa-reply"></i></button>
<button class="btn btn-sm btn-link" onclick="app.reactToMessage('${msg.message_id}','${msg.user_phone}')" title="Reaccionar"><i class="far fa-grin"></i></button>
</div>
<div class="message-time">
${time}
${msg.direction === 'outgoing' ? `<span class="message-status">${statusIcon}</span>` : ''}
</div>
</div>
</div>
`;
}).join('');
container.innerHTML = html;
}
getStatusIcon(status) {
switch (status) {
case 'sent': return '<i class="fas fa-check status-sent"></i>';
case 'delivered': return '<i class="fas fa-check-double status-delivered"></i>';
case 'read': return '<i class="fas fa-check-double status-read"></i>';
default: return '<i class="fas fa-clock status-sent"></i>';
}
}
async loadQuickReplies() {
const container = document.getElementById('quick-replies');
container.innerHTML = '';
try {
const resp = await fetch('api/get_templates.php?approved_only=1&limit=6');
const json = await resp.json();
if (json && json.success && Array.isArray(json.data)) {
json.data.forEach(t => {
const btn = document.createElement('button');
btn.className = 'btn btn-outline-primary btn-sm';
btn.style.marginRight = '6px';
btn.textContent = t.name;
btn.addEventListener('click', () => this.sendTemplateQuick(t.template_name, t.language_code || 'es'));
container.appendChild(btn);
});
}
} catch (e) {
console.error('Error loading quick replies', e);
}
}
async sendTemplateQuick(templateName, language) {
if (!this.currentUserId) return;
const conv = this.conversations.find(c => c.user_id === this.currentUserId);
const phone = conv ? conv.user_phone : document.getElementById('chat-phone').textContent;
try {
const resp = await fetch('api/send_message.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ recipient: phone, type: 'template', template_name: templateName, language })
});
const result = await resp.json();
if (result.success) {
await this.loadconversations(this.currentUserId, false);
await this.loadConversations();
} else {
alert('Error al enviar plantilla: ' + (result.error || JSON.stringify(result)));
}
} catch (e) {
console.error('Error sending template quick', e);
alert('Error al enviar plantilla');
}
}
scrollToBottom() {
const container = document.getElementById('chat-conversations');
container.scrollTop = container.scrollHeight;
}
async sendMessage() {
const input = document.getElementById('message-input');
const message = input.value.trim();
if (!message || !this.currentUserId) return;
// Deshabilitar input
input.disabled = true;
document.getElementById('send-btn').disabled = true;
try {
const replyTo = input.dataset.replyTo || null;
const response = await fetch('api/send_message.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: this.currentUserId,
message: message,
reply_to: replyTo
})
});
// Clear reply data attribute
input.dataset.replyTo = null;
const result = await response.json();
if (result.success) {
input.value = '';
// Recargar mensajes
await this.loadconversations(this.currentUserId, false);
// Actualizar lista de conversaciones
this.loadConversations();
// Actualizar respuestas rápidas
await this.loadQuickReplies();
} else {
alert('Error al enviar mensaje: ' + (result.error || 'Error desconocido'));
}
} catch (error) {
console.error('Error sending message:', error);
alert('Error al enviar mensaje');
} finally {
input.disabled = false;
document.getElementById('send-btn').disabled = false;
input.focus();
}
}
async loadQuickReplies() {
try {
const resp = await fetch('api/get_templates.php?approved_only=1&limit=10');
const json = await resp.json();
const container = document.getElementById('quick-replies');
if (!container) return;
container.innerHTML = '';
if (json && json.success && Array.isArray(json.data)) {
const templates = json.data.slice(0,5);
templates.forEach(t => {
const text = (t.body_text || t.name || t.template_name || '').replace(/\n/g,' ');
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-outline-secondary';
btn.dataset.qr = text;
btn.textContent = text.length > 30 ? text.substring(0,30) + '...' : text;
container.appendChild(btn);
});
}
} catch (err) {
console.error('loadQuickReplies error', err);
}
}
async promptEditUser() {
const currentName = document.getElementById('chat-name').textContent || '';
const newName = prompt('Nombre del usuario:', currentName);
if (!newName) return;
try {
const resp = await fetch('api/update_user.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: this.currentUserId, name: newName })
});
const json = await resp.json();
if (json && json.success) {
document.getElementById('chat-name').textContent = newName;
this.showSuccess('Usuario actualizado');
this.loadConversations();
} else {
this.showError('No se pudo actualizar usuario');
}
} catch (err) {
console.error(err);
this.showError('Error actualizando usuario');
}
}
async deleteCurrentConversation() {
if (!confirm('¿Eliminar esta conversación? Se borrará todo el historial.')) return;
try {
const resp = await fetch('api/delete_conversation.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: this.currentUserId })
});
const json = await resp.json();
if (json && json.success) {
this.showSuccess('Conversación eliminada');
// reset UI
document.getElementById('chat-area').style.display = 'none';
document.getElementById('no-conversation').style.display = 'block';
this.currentUserId = null;
this.loadConversations();
} else {
this.showError('No se pudo eliminar la conversación');
}
} catch (err) {
console.error(err);
this.showError('Error eliminando la conversación');
}
}
searchConversations(query) {
const items = document.querySelectorAll('.conversation-item');
items.forEach(item => {
const name = item.querySelector('.conversation-name').textContent.toLowerCase();
const preview = item.querySelector('.conversation-preview').textContent.toLowerCase();
const matches = name.includes(query.toLowerCase()) || preview.includes(query.toLowerCase());
item.style.display = matches ? 'flex' : 'none';
});
}
// ========== FUNCIONES MULTIMEDIA ==========
handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
// Validar tamaño
const maxSize = this.getMaxFileSize(file.type);
if (file.size > maxSize) {
alert(`El archivo es demasiado grande. Máximo: ${(maxSize / 1024 / 1024).toFixed(0)}MB`);
return;
}
// Mostrar preview
this.showMediaPreview(file);
}
getMaxFileSize(fileType) {
if (fileType.startsWith('image/')) return 5 * 1024 * 1024; // 5MB
if (fileType.startsWith('application/')) return 100 * 1024 * 1024; // 100MB
return 16 * 1024 * 1024; // 16MB para video/audio
}
showMediaPreview(file) {
const preview = document.getElementById('media-preview');
const thumbnail = document.getElementById('preview-thumbnail');
const filename = document.getElementById('preview-filename');
const filesize = document.getElementById('preview-filesize');
// Establecer información del archivo
filename.textContent = file.name;
filesize.textContent = this.formatFileSize(file.size);
// Mostrar thumbnail para imágenes
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
thumbnail.src = e.target.result;
thumbnail.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
thumbnail.style.display = 'none';
}
// Guardar archivo temporalmente
this.selectedFile = file;
// Mostrar preview
preview.style.display = 'block';
// Cambiar comportamiento del botón enviar
const sendBtn = document.getElementById('send-btn');
const originalHandler = sendBtn.onclick;
sendBtn.onclick = () => this.sendMediaMessage();
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
async sendMediaMessage() {
if (!this.selectedFile || !this.currentUserId) return;
const caption = document.getElementById('media-caption').value.trim();
const sendBtn = document.getElementById('send-btn');
const attachBtn = document.getElementById('attach-btn');
// Deshabilitar botones
sendBtn.disabled = true;
attachBtn.disabled = true;
sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
// 1. Subir archivo
const formData = new FormData();
formData.append('file', this.selectedFile);
const uploadResponse = await fetch('api/upload_media.php', {
method: 'POST',
body: formData
});
const uploadResult = await uploadResponse.json();
if (!uploadResult.success) {
throw new Error(uploadResult.error || 'Error subiendo archivo');
}
// 2. Enviar mensaje con el archivo
const user = this.conversations.find(c => c.user_id === this.currentUserId);
const phone = user ? user.phone_number : null;
if (!phone) {
throw new Error('No se encontró el número de teléfono');
}
const sendResponse = await fetch('api/send_media_message.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
recipient: phone,
media_url: uploadResult.data.url,
media_type: uploadResult.data.type,
caption: caption || null,
filename: this.selectedFile.name
})
});
const sendResult = await sendResponse.json();
if (!sendResult.success) {
throw new Error(sendResult.error || 'Error enviando mensaje');
}
// Éxito
this.cancelMediaUpload();
await this.loadconversations(this.currentUserId, false);
this.loadConversations();
} catch (error) {
console.error('Error sending media:', error);
alert('Error al enviar archivo: ' + error.message);
} finally {
sendBtn.disabled = false;
attachBtn.disabled = false;
sendBtn.innerHTML = '<i class="fas fa-paper-plane"></i>';
}
}
cancelMediaUpload() {
document.getElementById('media-preview').style.display = 'none';
document.getElementById('media-caption').value = '';
document.getElementById('file-input').value = '';
this.selectedFile = null;
// Restaurar botón enviar
const sendBtn = document.getElementById('send-btn');
sendBtn.onclick = null;
}
renderMediaMessage(message) {
if (!message.media_url) return '';
const mediaType = message.message_type;
const mediaUrl = message.media_url;
const caption = message.content;
switch (mediaType) {
case 'image':
// Usar miniatura local si existe, al abrir expandir con el archivo local completo o con media-url redirect
const thumb = msg.local_thumb ? (`/${msg.local_thumb}`) : (msg.local_file ? (`/${msg.local_file}`) : (msg.media_url_external || mediaUrl));
let full = '';
if (msg.local_file) {
full = `/api/version/media-url.php?local=${encodeURIComponent(msg.local_file)}`;
} else if (msg.media_url_external) {
// Si viene una ruta del tipo api/get_media.php?id=XXX extraer id
if (msg.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = msg.media_url_external.split('id=');
const mid = parts[1] ? decodeURIComponent(parts[1]) : '';
full = `/api/version/media-url.php?id=${encodeURIComponent(mid)}`;
} else if (/^https?:\/\//i.test(msg.media_url_external)) {
// URL directa
full = msg.media_url_external;
} else {
// fallback: pasar como url
full = `/api/version/media-url.php?url=${encodeURIComponent(msg.media_url_external)}`;
}
} else {
full = mediaUrl || '';
}
return `
<div class="message-media">
<a href="#" onclick="openImageLightbox(${JSON.stringify(full)}); return false;" title="Abrir imagen">
<img src="${thumb}" alt="Imagen" style="cursor:zoom-in">
</a>
</div>
${caption ? `<div>${caption}</div>` : ''}
`;
case 'video':
let videoSrc = '';
if (msg.local_file) {
videoSrc = `/${msg.local_file}`;
} else if (msg.media_url_external) {
if (msg.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = msg.media_url_external.split('id=');
const mid = parts[1] ? decodeURIComponent(parts[1]) : '';
videoSrc = `/api/version/media-url.php?id=${encodeURIComponent(mid)}`;
} else if (/^https?:\/\//i.test(msg.media_url_external)) {
videoSrc = msg.media_url_external;
} else {
videoSrc = `/api/version/media-url.php?url=${encodeURIComponent(msg.media_url_external)}`;
}
} else {
videoSrc = mediaUrl;
}
return `
<div class="message-media">
<video controls>
<source src="${videoSrc}" type="video/mp4">
Tu navegador no soporta video.
</video>
</div>
${caption ? `<div>${caption}</div>` : ''}
`;
case 'audio':
let audioSrc = '';
if (msg.local_file) {
audioSrc = `/${msg.local_file}`;
} else if (msg.media_url_external) {
if (msg.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = msg.media_url_external.split('id=');
const mid = parts[1] ? decodeURIComponent(parts[1]) : '';
audioSrc = `/api/version/media-url.php?id=${encodeURIComponent(mid)}`;
} else if (/^https?:\/\//i.test(msg.media_url_external)) {
audioSrc = msg.media_url_external;
} else {
audioSrc = `/api/version/media-url.php?url=${encodeURIComponent(msg.media_url_external)}`;
}
} else {
audioSrc = mediaUrl;
}
return `
<div class="message-media">
<audio controls>
<source src="${audioSrc}" type="audio/mpeg">
Tu navegador no soporta audio.
</audio>
</div>
`;
case 'document':
// Para documentos no descargamos automáticamente; abrir la URL externa (o la redirección por media id) para descargar
let docUrl = '';
if (msg.local_file) {
docUrl = `/api/version/media-url.php?local=${encodeURIComponent(msg.local_file)}&download=1`;
} else if (msg.media_url_external) {
if (msg.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = msg.media_url_external.split('id=');
const mid = parts[1] ? decodeURIComponent(parts[1]) : '';
docUrl = `/api/version/media-url.php?id=${encodeURIComponent(mid)}&download=1`;
} else if (/^https?:\/\//i.test(msg.media_url_external)) {
docUrl = `/api/version/media-url.php?url=${encodeURIComponent(msg.media_url_external)}&download=1`;
} else {
// fallback
docUrl = `/api/version/media-url.php?url=${encodeURIComponent(msg.media_url_external)}&download=1`;
}
}
if (!docUrl) {
return `
<div class="message-document disabled">
<i class="fas fa-file-pdf"></i>
<div class="document-info">
<div class="document-name">${caption || 'Documento'}</div>
<div class="document-size text-muted">No disponible para descargar</div>
</div>
</div>
`;
}
return `
<div class="message-document">
<i class="fas fa-file-pdf"></i>
<div class="document-info">
<div class="document-name">${caption || 'Documento'}</div>
<div class="document-size">
<a href="${docUrl}" target="_blank" rel="noopener noreferrer" ${docUrl && docUrl.indexOf('download=1') !== -1 ? 'download' : ''}>Descargar</a>
</div>
</div>
<i class="fas fa-download"></i>
</div>
`;
default:
return caption || '';
}
}
}
// Función global para cancelar
function cancelMediaUpload() {
if (window.chatApp) {
window.chatApp.cancelMediaUpload();
}
}
// Inicializar cuando la página cargue
let chat;
document.addEventListener('DOMContentLoaded', () => {
chat = new WhatsAppChat();
window.chatApp = chat; // Exponer globalmente para funciones auxiliares
});
</script>
</body>
</html>