diff --git a/conversations.php b/conversations.php index 0f30cf3..0ee1bb0 100644 --- a/conversations.php +++ b/conversations.php @@ -564,6 +564,22 @@ .notification-toast.urgent { border-left: 4px solid #e74c3c; } + /* Visual destacado para notificaciones de tipo "attention" (ej. usuario subió documentos) */ + .notification-toast.attention { + background: linear-gradient(90deg, #fff7e6, #fff3e0); + border: 1px solid rgba(255,165,0,0.18); + color: #333; + box-shadow: 0 8px 24px rgba(255,165,0,0.06); + min-width: 240px; + } + .notification-toast.attention .nt-icon { + font-size: 18px; + margin-right: 8px; + transform: translateY(-1px); + color: #ff9900; + } + .notification-toast.attention .nt-actions .btn { min-width: 70px; } + .message-template { background: #fffacd; border: 1px solid #f0e68c; @@ -572,6 +588,31 @@ margin-bottom: 10px; font-size: 13px; } + .message-template.system { + background: linear-gradient(90deg,#fff4e6,#fffaf0); + border: 1px solid rgba(255,165,0,0.18); + color: #5c3a00; + font-weight: 600; + display: flex; + gap: 8px; + align-items: center; + animation: systemPop 320ms ease; + } + .message-template.system .mt-icon { font-size:16px; margin-right:6px; } + @keyframes systemPop { from { transform: translateY(6px); opacity:0 } to { transform: translateY(0); opacity:1 } } + + /* Highlight animation for newly inserted system messages */ + .message-template.system.system-highlight { + box-shadow: 0 10px 30px rgba(255,165,0,0.12); + border-color: rgba(255,165,0,0.28); + transform-origin: center left; + animation: pulseHighlight 1s ease both; + } + @keyframes pulseHighlight { + 0% { transform: translateY(6px) scale(.995); opacity: 0.95; } + 50% { transform: translateY(0) scale(1.01); opacity: 1; } + 100% { transform: translateY(0) scale(1); opacity: 1; } + } .unread-count { background: var(--whatsapp-green); @@ -1066,12 +1107,20 @@ const toast = document.createElement('div'); // compact by default toast.className = 'notification-toast small'; + // Detect special "attention" notifications (e.g. user sent documents or similar system events) + const isAttention = ( + // explicit attention flags + notification.type === 'attention' || notification.system === 'attention' || notification.level === 'attention' || notification.attention || + // known event types/tags for user-sent documents + notification.type === 'usersentdocuments' || notification.tag === 'usersentdocuments' || notification.system === 'usersentdocuments' + ); if (notification.cool || notification.type === 'cool') toast.classList.add('cool'); if (notification.level === 'urgent' || notification.urgent) toast.classList.add('urgent'); + if (isAttention) toast.classList.add('attention'); const iconSpan = document.createElement('span'); iconSpan.className = 'nt-icon'; - iconSpan.textContent = notification.icon || (notification.cool ? '✨' : (notification.level === 'urgent' ? '⚠️' : '🔔')); + iconSpan.textContent = notification.icon || (isAttention ? '📎' : (notification.cool ? '✨' : (notification.level === 'urgent' ? '⚠️' : '🔔'))); toast.appendChild(iconSpan); const msg = document.createElement('div'); @@ -1090,8 +1139,8 @@ }; const openBtn = document.createElement('button'); - openBtn.className = notification.cool ? 'btn btn-sm btn-light' : 'btn btn-sm btn-primary'; - openBtn.textContent = 'Abrir'; + openBtn.className = notification.cool ? 'btn btn-sm btn-light' : (isAttention ? 'btn btn-sm btn-warning' : 'btn btn-sm btn-primary'); + openBtn.textContent = (notification.open_label && notification.open_label !== 'Ver') ? notification.open_label : 'Abrir'; openBtn.onclick = async () => { // Try ack on server; if fails, schedule retry. Always mark as dismissed locally so it won't reappear. try { @@ -1104,11 +1153,14 @@ } } catch (e) { this._pendingAck.set(nid, notification); } removeToastLocal(); - // navigate to conv if present + // navigate to conv or open media if present let data = {}; try { data = notification.data ? JSON.parse(notification.data) : {}; } catch(e) {} const userId = data.user_id || notification.user_id; - if (userId) { + const fileUrl = data.file_url || data.url || null; + if (fileUrl && this.currentUserId && String(this.currentUserId) === String(userId)) { + try { openMediaLightbox(fileUrl, data.file_name || ''); } catch (e) { console.warn('openMediaLightbox failed', e); } + } else if (userId) { const conv = this.conversations.find(c => c.user_id == userId); if (conv) { this.openConversation(conv.user_id, conv.name, conv.phone_number); @@ -1116,7 +1168,7 @@ this.openConversation(userId, 'Usuario', ''); } } - }; + }; const dismiss = document.createElement('button'); dismiss.className = 'btn btn-sm btn-outline-secondary'; @@ -1141,6 +1193,15 @@ container.appendChild(toast); + // If attention notification and related to current open conversation, insert a system message into the chat view + try { + const dataObj = notification.data ? (typeof notification.data === 'string' ? JSON.parse(notification.data) : notification.data) : {}; + const userId = dataObj.user_id || notification.user_id; + if (isAttention && userId && this.currentUserId && String(this.currentUserId) === String(userId) && typeof this.showSystemNotificationInChat === 'function') { + this.showSystemNotificationInChat(notification); + } + } catch (e) { console.warn('showNotificationToast -> showSystemNotificationInChat failed', e); } + // Auto remove (shorter for compact notifications) with minimum enforced const _minToastDuration = 3000; let timeout = notification.duration ? Number(notification.duration) : (notification.cool ? 15000 : 8000); @@ -1163,6 +1224,82 @@ }, timeout); } + showSystemNotificationInChat(notification) { + try { + const data = notification.data ? (typeof notification.data === 'string' ? JSON.parse(notification.data) : notification.data) : {}; + const userId = data.user_id || notification.user_id; + // Only show in currently open conversation + if (!userId || String(this.currentUserId) !== String(userId)) return; + + const container = document.getElementById('chat-conversations'); + if (!container) return; + + const el = document.createElement('div'); + el.className = 'message incoming'; + el.dataset.notificationId = String(notification.id || ''); + + const bubble = document.createElement('div'); + bubble.className = 'message-bubble message-template system'; + + // Friendly title and message (prefer explicit fields if present) + const title = notification.title || data.title || 'Documentos recibidos'; + const msg = notification.message || data.text || data.message || (data.summary || 'El usuario envió documentos.'); + + // File info (if available) + const fileName = data.file_name || data.filename || data.file || null; + const fileUrl = data.file_url || data.url || null; + const fileSize = data.file_size || data.size || null; + + // Build inner HTML + const parts = []; + parts.push(`📎`); + parts.push(`
`); + parts.push(`
${escapeHtml(String(title))}
`); + parts.push(`
${escapeHtml(String(msg))}
`); + if (fileName) { + parts.push(`
+ +
+
${escapeHtml(fileName)}
+
${fileSize ? this.formatFileSize(Number(fileSize)) : ''}
+
+
`); + } + parts.push(`
`); + + // Actions + parts.push(`
`); + if (fileUrl) { + parts.push(`Abrir`); + parts.push(``); + } + parts.push(`
`); + + bubble.innerHTML = parts.join(''); + el.appendChild(bubble); + + container.appendChild(el); + this.scrollToBottom(); + + // Attach handlers + + const downloadBtn = bubble.querySelector('.download-doc-btn'); + if (downloadBtn) downloadBtn.addEventListener('click', () => { + if (fileUrl) { + const a = document.createElement('a'); + a.href = fileUrl; + a.download = fileName || ''; + document.body.appendChild(a); + a.click(); + a.remove(); + } + }); + + } catch (e) { + console.warn('showSystemNotificationInChat failed', e); + } + } + setupEventListeners() { // Búsqueda de conversaciones document.getElementById('search-input').addEventListener('input', (e) => { @@ -2602,21 +2739,75 @@ replyHtml = `
En respuesta a: ${window.escapeHtml(previewText)}
`; } const mediaPresent = (msg.local_thumb || msg.local_file || msg.media_url_external || msg.media_url); - let content = mediaPresent - ? (window.renderMediaMessage ? window.renderMediaMessage(msg) : (window.formatMessageContent ? window.formatMessageContent(msg.content || '[Mensaje]') : window.escapeHtml(msg.content || '[Mensaje]'))) - : (window.formatMessageContent ? window.formatMessageContent(msg.content || '') : window.escapeHtml(msg.content || '')); - // If after formatting the content is empty (spaces or nothing), show a clearer placeholder. - // But allow pure media-only content (images/audio/video) to render instead of being treated as empty. + // Try to render system-style messages when the message content is a JSON payload + let content = ''; + let isSystemJson = false; try { - const tmp = (content || '').replace(/<[^>]*>/g, '').trim(); // strip tags and check - const hasMediaTag = /<(img|audio|video|source|a[^>]*download|div\s+class=["']?message-media)/i.test(content || ''); - if (!tmp && !hasMediaTag) { - console.warn('Rendered message content empty, replacing with placeholder', mid, msg.id, msg.created_at); - content = '(Mensaje sin texto)'; + const raw = (typeof msg.content === 'string') ? msg.content.trim() : ''; + if (!mediaPresent && raw && (raw.startsWith('{') || raw.startsWith('['))) { + try { + const parsed = JSON.parse(raw); + const isSys = parsed && (parsed.type === 'usersentdocuments' || parsed.system === 'usersentdocuments' || parsed.type === 'attention' || parsed.system === 'attention' || parsed.attention); + if (isSys) { + isSystemJson = true; + const title = parsed.title || parsed.message || parsed.text || parsed.summary || 'Documentos recibidos'; + const msgText = parsed.message || parsed.text || parsed.summary || ''; + const fileName = parsed.file_name || parsed.filename || parsed.file || null; + const fileUrl = parsed.file_url || parsed.url || null; + const fileSize = parsed.file_size || parsed.size || null; + + const parts = []; + parts.push(`
`); + parts.push(`📎`); + parts.push(`
`); + parts.push(`
${escapeHtml(String(title))}
`); + if (msgText) parts.push(`
${escapeHtml(String(msgText))}
`); + if (fileName) { + parts.push(`
+ +
+
${escapeHtml(fileName)}
+
${fileSize ? this.formatFileSize(Number(fileSize)) : ''}
+
+
`); + } + parts.push(`
`); + parts.push(`
`); + if (fileUrl) { + parts.push(`Abrir`); + parts.push(``); + } + parts.push(`
`); + parts.push(`
`); + + content = parts.join(''); + } + } catch (e) { + // not JSON or parse error — fall back to normal rendering + } } } catch (e) { /* ignore */ } + if (!isSystemJson) { + content = mediaPresent + ? (window.renderMediaMessage ? window.renderMediaMessage(msg) : (window.formatMessageContent ? window.formatMessageContent(msg.content || '[Mensaje]') : window.escapeHtml(msg.content || '[Mensaje]'))) + : (window.formatMessageContent ? window.formatMessageContent(msg.content || '') : window.escapeHtml(msg.content || '')); + + // If after formatting the content is empty (spaces or nothing), show a clearer placeholder. + // But allow pure media-only content (images/audio/video) to render instead of being treated as empty. + try { + const tmp = (content || '').replace(/<[^>]*>/g, '').trim(); // strip tags and check + const hasMediaTag = /<(img|audio|video|source|a[^>]*download|div\s+class=["']?message-media)/i.test(content || ''); + if (!tmp && !hasMediaTag) { + console.warn('Rendered message content empty, replacing with placeholder', mid, msg.id, msg.created_at); + content = '(Mensaje sin texto)'; + } + } catch (e) { /* ignore */ } + } else { + // If it is the system JSON, we may want to highlight it briefly after insertion later + } + const time = msg.created_at ? new Date(msg.created_at).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }) : ''; const statusIcon = this.getStatusIcon(msg.status); const reactionHtml = msg.reaction_emoji ? `
${msg.reaction_emoji}
` : ''; @@ -2640,6 +2831,34 @@ } else { container.appendChild(div); } + + // Attach handlers for system-like messages rendered from JSON (view/download buttons + highlight) + try { + const raw = (typeof msg.content === 'string') ? msg.content.trim() : ''; + if (raw && (raw.startsWith('{') || raw.startsWith('['))) { + const parsed = JSON.parse(raw); + const isSys = parsed && (parsed.type === 'usersentdocuments' || parsed.system === 'usersentdocuments' || parsed.type === 'attention' || parsed.system === 'attention' || parsed.attention); + if (isSys) { + const bubble = div.querySelector('.message-template.system') || div.querySelector('.message-template'); + if (bubble) { + const viewBtn = bubble.querySelector('.view-docs-btn'); + const downloadBtn = bubble.querySelector('.download-doc-btn'); + const fileUrl = parsed.file_url || parsed.url || null; + const userId = parsed.user_id || parsed.data && parsed.data.user_id || null; + + if (downloadBtn) downloadBtn.addEventListener('click', () => { + if (fileUrl) { + const a = document.createElement('a'); a.href = fileUrl; a.download = parsed.file_name || ''; document.body.appendChild(a); a.click(); a.remove(); + } + }); + + // brief visual highlight + bubble.classList.add('system-highlight'); + setTimeout(() => bubble.classList.remove('system-highlight'), 4200); + } + } + } + } catch (e) { /* ignore */ } } catch (e) { console.warn('create message failed', e); } } }