Reemplaza /instance/status → /status, /instance/create → /connect, etc. Lee QR de data.qr (formato del bridge) ademas de data.base64 (Evolution). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
372 lines
12 KiB
JavaScript
372 lines
12 KiB
JavaScript
/**
|
|
* 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('chatMessages');
|
|
var chatHeader = document.getElementById('chatHeaderPhone');
|
|
var chatInput = document.getElementById('chatInput');
|
|
var sendBtn = document.getElementById('sendBtn');
|
|
var newChatBtn = document.getElementById('newChatBtn');
|
|
var emptyState = document.getElementById('emptyState');
|
|
var chatPanel = 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';
|
|
} 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 = '<div class="conv-empty">No hay conversaciones</div>';
|
|
return;
|
|
}
|
|
var html = '';
|
|
convs.forEach(function (c) {
|
|
var isActive = c.phone === activePhone;
|
|
var dirIcon = c.last_direction === 'outgoing' ? '→ ' : '';
|
|
html += '<div class="conv-item' + (isActive ? ' is-active' : '') + '" data-phone="' + escHtml(c.phone) + '">'
|
|
+ '<div class="conv-item__phone">' + escHtml(fmtPhone(c.phone)) + '</div>'
|
|
+ '<div class="conv-item__preview">' + dirIcon + escHtml(c.last_message) + '</div>'
|
|
+ '<div class="conv-item__time">' + fmtTime(c.last_at) + '</div>'
|
|
+ '</div>';
|
|
});
|
|
convList.innerHTML = html;
|
|
|
|
convList.querySelectorAll('.conv-item').forEach(function (el) {
|
|
el.addEventListener('click', function () {
|
|
openConversation(el.getAttribute('data-phone'));
|
|
});
|
|
});
|
|
}).catch(function () {
|
|
convList.innerHTML = '<div class="conv-empty">Error cargando conversaciones</div>';
|
|
});
|
|
}
|
|
|
|
// -- Open a conversation ---------------------------------------------------
|
|
|
|
function openConversation(phone) {
|
|
activePhone = phone;
|
|
chatHeader.textContent = fmtPhone(phone);
|
|
emptyState.style.display = 'none';
|
|
chatPanel.style.display = 'flex';
|
|
|
|
convList.querySelectorAll('.conv-item').forEach(function (el) {
|
|
el.classList.toggle('is-active', el.getAttribute('data-phone') === phone);
|
|
});
|
|
|
|
loadMessages(phone);
|
|
startPolling();
|
|
}
|
|
|
|
function loadMessages(phone) {
|
|
api('GET', '/conversations/' + encodeURIComponent(phone)).then(function (data) {
|
|
var msgs = data.messages || [];
|
|
renderMessages(msgs);
|
|
});
|
|
}
|
|
|
|
function renderMessages(msgs) {
|
|
var html = '';
|
|
msgs.forEach(function (m) {
|
|
var cls = m.direction === 'outgoing' ? 'msg-bubble--out' : 'msg-bubble--in';
|
|
var statusBadge = '';
|
|
if (m.direction === 'outgoing' && m.status) {
|
|
statusBadge = '<span class="msg-status">' + escHtml(m.status) + '</span>';
|
|
}
|
|
html += '<div class="msg-bubble ' + cls + '">'
|
|
+ '<div class="msg-bubble__text">' + escHtml(m.message_text).replace(/\n/g, '<br>') + '</div>'
|
|
+ '<div class="msg-bubble__meta">' + fmtTime(m.created_at) + ' ' + statusBadge + '</div>'
|
|
+ '</div>';
|
|
});
|
|
chatMessages.innerHTML = html || '<div class="chat-empty">Sin mensajes</div>';
|
|
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; }
|
|
var quoteId = prompt('ID de la cotizacion a enviar:');
|
|
if (!quoteId) return;
|
|
api('POST', '/send-quote/' + quoteId, { phone: activePhone }).then(function (res) {
|
|
if (res.error) {
|
|
alert('Error: ' + 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) {}
|
|
|
|
})();
|