/** * whatsapp.js β€” WhatsApp via Evolution API * * Connection flow: Create instance -> Scan QR -> Connected * Left panel: conversation list (phone numbers + last message preview) * Right panel: chat view with message bubbles * Bottom: text input + send button */ (function () { 'use strict'; var token = localStorage.getItem('pos_token'); if (!token) { window.location.href = '/pos/login'; return; } var API = '/pos/api/whatsapp'; var activePhone = null; var pollTimer = null; var statusPollTimer = null; var connectionState = 'unknown'; // 'open', 'close', 'connecting', 'unknown' // -- Helpers --------------------------------------------------------------- function authHeaders() { return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }; } function api(method, path, body) { var opts = { method: method, headers: authHeaders() }; if (body) opts.body = JSON.stringify(body); return fetch(API + path, opts).then(function (r) { if (r.status === 401) { window.location.href = '/pos/login'; } return r.json(); }); } function escHtml(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } function fmtTime(iso) { if (!iso) return ''; var d = new Date(iso); var now = new Date(); var isToday = d.toDateString() === now.toDateString(); if (isToday) { return d.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }); } return d.toLocaleDateString('es-MX', { day: '2-digit', month: 'short' }) + ' ' + d.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }); } function fmtPhone(phone) { if (!phone) return ''; if (phone.length === 13 && phone.startsWith('521')) { return '+52 1 ' + phone.slice(3, 5) + ' ' + phone.slice(5, 9) + ' ' + phone.slice(9); } if (phone.length === 12 && phone.startsWith('52')) { return '+52 ' + phone.slice(2, 4) + ' ' + phone.slice(4, 8) + ' ' + phone.slice(8); } return '+' + phone; } // -- DOM refs -------------------------------------------------------------- var convList = document.getElementById('convList'); var chatMessages = document.getElementById('waChatMessages') || document.getElementById('chatMessages'); var chatHeader = document.getElementById('chatHeaderPhone'); var chatInput = document.getElementById('waChatInput') || document.getElementById('chatInput'); var sendBtn = document.getElementById('waSendBtn') || document.getElementById('sendBtn'); var newChatBtn = document.getElementById('newChatBtn'); var emptyState = document.getElementById('emptyState'); var chatPanel = document.getElementById('waChatPanel') || document.getElementById('chatPanel'); var statusDot = document.getElementById('statusDot'); var statusText = document.getElementById('statusText'); var connectSection = document.getElementById('connectSection'); var messengerArea = document.getElementById('messengerArea'); var qrImg = document.getElementById('qrImg'); var qrPlaceholder = document.getElementById('qrPlaceholder'); var connectBtn = document.getElementById('connectBtn'); var disconnectBtn = document.getElementById('disconnectBtn'); var refreshQrBtn = document.getElementById('refreshQrBtn'); // -- Connection management ------------------------------------------------- function checkInstanceStatus() { api('GET', '/status').then(function (data) { var state = (data.instance || data).state || data.state || 'close'; updateConnectionUI(state); }).catch(function () { updateConnectionUI('close'); }); } function updateConnectionUI(state) { connectionState = state; if (state === 'open') { statusDot.className = 'status-dot status-dot--ok'; statusText.textContent = 'Conectado'; connectSection.style.display = 'none'; messengerArea.style.display = 'flex'; disconnectBtn.style.display = ''; connectBtn.style.display = 'none'; // Load conversations + start polling on page load / reconnect loadConversations(); startPolling(); } else if (state === 'connecting') { statusDot.className = 'status-dot status-dot--warn'; statusText.textContent = 'Escaneando QR...'; connectSection.style.display = 'flex'; messengerArea.style.display = 'none'; disconnectBtn.style.display = 'none'; connectBtn.style.display = 'none'; refreshQrBtn.style.display = ''; } else { // close or unknown statusDot.className = 'status-dot status-dot--error'; statusText.textContent = 'Desconectado'; connectSection.style.display = 'flex'; messengerArea.style.display = 'none'; disconnectBtn.style.display = 'none'; connectBtn.style.display = ''; refreshQrBtn.style.display = 'none'; qrImg.style.display = 'none'; qrPlaceholder.style.display = ''; } } function doConnect() { connectBtn.disabled = true; connectBtn.textContent = 'Creando instancia...'; api('POST', '/connect').then(function (data) { connectBtn.disabled = false; connectBtn.textContent = 'Conectar WhatsApp'; if (data.error) { alert('Error: ' + (data.error.message || data.error)); return; } // Instance created, now fetch QR fetchQR(); }).catch(function () { connectBtn.disabled = false; connectBtn.textContent = 'Conectar WhatsApp'; alert('Error de red al crear instancia'); }); } function fetchQR() { qrPlaceholder.textContent = 'Generando QR...'; api('GET', '/qr').then(function (data) { var base64 = data.qr || data.base64 || data.qrcode || ''; if (base64) { qrImg.src = base64.startsWith('data:') ? base64 : 'data:image/png;base64,' + base64; qrImg.style.display = 'block'; qrPlaceholder.style.display = 'none'; refreshQrBtn.style.display = ''; updateConnectionUI('connecting'); // Start polling for connection state while QR is shown startStatusPolling(); } else if ((data.instance && data.instance.state === 'open') || data.state === 'open') { // Already connected updateConnectionUI('open'); loadConversations(); } else { qrPlaceholder.textContent = 'No se pudo generar el QR. Intenta de nuevo.'; qrPlaceholder.style.display = ''; qrImg.style.display = 'none'; } }).catch(function () { qrPlaceholder.textContent = 'Error al obtener QR'; }); } function doDisconnect() { if (!confirm('Desconectar WhatsApp?')) return; api('POST', '/logout').then(function () { updateConnectionUI('close'); stopStatusPolling(); }); } function startStatusPolling() { stopStatusPolling(); statusPollTimer = setInterval(function () { api('GET', '/status').then(function (data) { var state = (data.instance || data).state || data.state || 'close'; if (state === 'open') { updateConnectionUI('open'); stopStatusPolling(); loadConversations(); startPolling(); } }); }, 3000); } function stopStatusPolling() { if (statusPollTimer) { clearInterval(statusPollTimer); statusPollTimer = null; } } connectBtn.addEventListener('click', doConnect); disconnectBtn.addEventListener('click', doDisconnect); refreshQrBtn.addEventListener('click', fetchQR); // -- Load conversations ---------------------------------------------------- function loadConversations() { api('GET', '/conversations').then(function (data) { var convs = data.conversations || []; if (convs.length === 0) { convList.innerHTML = '
No hay conversaciones
'; return; } var html = ''; convs.forEach(function (c) { var isActive = c.phone === activePhone; var dirIcon = c.last_direction === 'outgoing' ? 'β†— ' : '↙ '; // Show contact name if available, otherwise try to format the phone. // LID numbers (15+ digits, no country code pattern) show as "Contacto" var displayName = c.contact_name || ''; if (!displayName) { var isLid = c.phone.length > 13 || !/^(52|1|44|34)/.test(c.phone); displayName = isLid ? 'Contacto WhatsApp' : fmtPhone(c.phone); } html += '
' + '
' + escHtml(displayName) + '
' + '
' + dirIcon + escHtml(c.last_message || '(sin texto)') + '
' + '
' + fmtTime(c.last_at) + '
' + '' + '
'; }); // "Borrar todo" button at the bottom html += '
' + '' + '
'; convList.innerHTML = html; convList.querySelectorAll('.conv-item').forEach(function (el) { el.addEventListener('click', function (e) { if (e.target.classList.contains('conv-item__delete')) return; var name = el.querySelector('.conv-item__phone') ? el.querySelector('.conv-item__phone').textContent : ''; openConversation(el.getAttribute('data-phone'), name); }); }); // Wire delete buttons convList.querySelectorAll('.conv-item__delete').forEach(function (btn) { btn.addEventListener('click', function (e) { e.stopPropagation(); var phone = btn.getAttribute('data-del-phone'); if (confirm('Borrar conversacion con ' + fmtPhone(phone) + '?')) { deleteConversation(phone); } }); }); }).catch(function () { convList.innerHTML = '
Error cargando conversaciones
'; }); } function deleteConversation(phone) { api('DELETE', '/conversations/' + encodeURIComponent(phone)).then(function (res) { if (res.ok) { if (activePhone === phone) { activePhone = null; chatPanel.style.display = 'none'; emptyState.style.display = ''; if (messengerArea) messengerArea.classList.remove('has-active-chat'); } loadConversations(); } else { alert('Error: ' + (res.error || 'unknown')); } }); } window.deleteAllConversations = function () { if (!confirm('Borrar TODAS las conversaciones? Esta accion no se puede deshacer.')) return; api('DELETE', '/conversations').then(function (res) { if (res.ok) { activePhone = null; chatPanel.style.display = 'none'; emptyState.style.display = ''; loadConversations(); } }); }; // -- Open a conversation --------------------------------------------------- var activeContactName = ''; function openConversation(phone, contactName) { try { console.log('[WA-UI] Opening conversation:', phone, contactName); activePhone = phone; // Use contact name if available; fall back to formatted phone var isLid = phone.length > 13 || !/^(52|1|44|34)/.test(phone); activeContactName = contactName || ''; chatHeader.textContent = activeContactName || (isLid ? 'Contacto WhatsApp' : fmtPhone(phone)); emptyState.style.display = 'none'; chatPanel.style.display = 'flex'; console.log('[WA-UI] chatPanel display set to flex. chatPanel element:', chatPanel ? 'exists' : 'null'); // Add has-active-chat class for mobile responsive layout if (messengerArea) messengerArea.classList.add('has-active-chat'); convList.querySelectorAll('.conv-item').forEach(function (el) { el.classList.toggle('is-active', el.getAttribute('data-phone') === phone); }); loadMessages(phone); startPolling(); } catch (e) { console.error('[WA-UI] openConversation error:', e); } } function loadMessages(phone) { console.log('[WA-UI] loadMessages start:', phone); api('GET', '/conversations/' + encodeURIComponent(phone)).then(function (data) { console.log('[WA-UI] loadMessages response:', data); if (data.error) { console.error('[WA-UI] loadMessages error:', data.error); chatMessages.innerHTML = '
Error cargando mensajes: ' + escHtml(data.error) + '
'; return; } var msgs = data.messages || []; console.log('[WA-UI] loadMessages messages count:', msgs.length); renderMessages(msgs); }).catch(function (err) { console.error('[WA-UI] loadMessages network error:', err); chatMessages.innerHTML = '
Error de red al cargar mensajes
'; }); } function renderMessages(msgs) { console.log('[WA-UI] renderMessages called with', msgs.length, 'messages'); if (!chatMessages) { console.error('[WA-UI] chatMessages element is null!'); return; } var html = ''; msgs.forEach(function (m) { var cls = m.direction === 'outgoing' ? 'msg-bubble--out' : 'msg-bubble--in'; var text = m.message_text || m.text || ''; var time = m.created_at || m.date || ''; html += '
' + '
' + escHtml(text).replace(/\n/g, '
') + '
' + '
' + fmtTime(time) + '
' + '
'; }); console.log('[WA-UI] renderMessages HTML length:', html.length); chatMessages.innerHTML = html || '
Sin mensajes
'; chatMessages.scrollTop = chatMessages.scrollHeight; } // -- Send message ---------------------------------------------------------- function doSend() { var text = chatInput.value.trim(); if (!text || !activePhone) return; chatInput.value = ''; sendBtn.disabled = true; api('POST', '/send', { phone: activePhone, message: text }).then(function (res) { sendBtn.disabled = false; if (res.error) { alert('Error: ' + res.error); } else { loadMessages(activePhone); loadConversations(); } }).catch(function () { sendBtn.disabled = false; alert('Error de red al enviar mensaje'); }); } sendBtn.addEventListener('click', doSend); chatInput.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); doSend(); } }); // -- New conversation ------------------------------------------------------ newChatBtn.addEventListener('click', function () { var phone = prompt('Numero de telefono (formato: 5215512345678):'); if (phone) { phone = phone.replace(/[\s\-\+\(\)]/g, ''); openConversation(phone); loadConversations(); } }); // -- Send quotation -------------------------------------------------------- var quoteBtn = document.getElementById('sendQuoteBtn'); if (quoteBtn) { quoteBtn.addEventListener('click', function () { if (!activePhone) { alert('Selecciona una conversacion primero'); return; } // Fetch available quotations and let user pick one fetch('/pos/api/quotations?per_page=20', { headers: authHeaders() }) .then(function (r) { return r.json(); }) .then(function (d) { var quotes = (d.data || []).filter(function (q) { return q.status === 'active'; }); if (quotes.length === 0) { alert('No hay cotizaciones activas. Crea una desde el POS (F4) o via WhatsApp.'); return; } var msg = 'Cotizaciones activas:\n'; quotes.forEach(function (q) { msg += '#' + q.id + ' β€” $' + q.total.toFixed(2) + ' (' + (q.customer_name || q.source || 'sin cliente') + ')\n'; }); var quoteId = prompt(msg + '\nEscribe el ID de la cotizacion a enviar:'); if (!quoteId) return; // Fetch the quotation detail and send it formatted fetch('/pos/api/quotations/' + quoteId, { headers: authHeaders() }) .then(function (r) { return r.json(); }) .then(function (q) { if (q.error) { alert('Error: ' + q.error); return; } // Format the quotation as a WhatsApp message var lines = ['πŸ“„ *COTIZACIΓ“N #' + q.id + '*', '']; (q.items || []).forEach(function (it, i) { lines.push((i + 1) + '. ' + it.name); lines.push(' #' + it.part_number + ' Γ— ' + it.quantity + ' = $' + it.subtotal.toFixed(2)); }); lines.push('─────────────'); lines.push('Subtotal: $' + q.subtotal.toFixed(2)); lines.push('IVA: $' + q.tax_total.toFixed(2)); lines.push('*TOTAL: $' + q.total.toFixed(2) + '*'); var text = lines.join('\n'); api('POST', '/send', { phone: activePhone, message: text }).then(function (res) { if (res.error) { alert('Error enviando: ' + res.error); } else { loadMessages(activePhone); loadConversations(); } }); }); }); }); } // -- Polling for new messages ---------------------------------------------- function startPolling() { if (pollTimer) clearInterval(pollTimer); pollTimer = setInterval(function () { if (activePhone) loadMessages(activePhone); loadConversations(); }, 10000); } // -- Init ------------------------------------------------------------------ checkInstanceStatus(); // Also check periodically (every 30s) in case connection drops setInterval(checkInstanceStatus, 30000); // -- User info for sidebar ------------------------------------------------- try { var payload = JSON.parse(atob(token.split('.')[1])); window.POS_USER = { name: payload.name || 'Usuario', roleLabel: (payload.role || '').charAt(0).toUpperCase() + (payload.role || '').slice(1), initials: (payload.name || 'U').split(' ').map(function(w){return w[0]}).join('').slice(0,2).toUpperCase() }; } catch(e) {} })();