Files
Autoparts-DB/pos/static/js/whatsapp2.js

537 lines
20 KiB
JavaScript
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.
/**
* 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 qrPollTimer = 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);
// If bridge already has a QR ready, show it immediately
if (state === 'qr' || state === 'connecting') {
fetchQR();
}
}).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();
stopQRPolling();
} else if (state === 'connecting' || state === 'qr') {
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 = '';
stopQRPolling();
}
}
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;
}
// Switch UI to connecting state immediately
updateConnectionUI('connecting');
qrPlaceholder.textContent = 'Iniciando conexion con WhatsApp, generando QR...';
qrPlaceholder.style.display = '';
qrImg.style.display = 'none';
// Start polling for QR; the first fetchQR may not have QR ready yet
startStatusPolling();
startQRPolling();
}).catch(function () {
connectBtn.disabled = false;
connectBtn.textContent = 'Conectar WhatsApp';
alert('Error de red al crear instancia');
});
}
function fetchQR() {
// Only update placeholder text if we don't already have a QR image showing
if (qrImg.style.display !== 'block') {
qrPlaceholder.textContent = 'Generando codigo QR, espera unos segundos...';
}
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();
startQRPolling();
} else if ((data.instance && data.instance.state === 'open') || data.state === 'open') {
// Already connected
updateConnectionUI('open');
loadConversations();
} else {
// QR not ready yet — this is normal right after pressing Connect
if (qrImg.style.display !== 'block') {
qrPlaceholder.textContent = 'Generando codigo QR, por favor espera... (el codigo cambia cada pocos segundos, escanealo en cuanto aparezca)';
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;
}
}
function startQRPolling() {
stopQRPolling();
qrPollTimer = setInterval(function () {
if (connectionState === 'connecting' || connectionState === 'qr') {
fetchQR();
} else {
stopQRPolling();
}
}, 5000);
}
function stopQRPolling() {
if (qrPollTimer) {
clearInterval(qrPollTimer);
qrPollTimer = 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' ? '↗ ' : '↙ ';
// 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 += '<div class="conv-item' + (isActive ? ' is-active' : '') + '" data-phone="' + escHtml(c.phone) + '">'
+ '<div class="conv-item__phone">' + escHtml(displayName) + '</div>'
+ '<div class="conv-item__preview">' + dirIcon + escHtml(c.last_message || '(sin texto)') + '</div>'
+ '<div class="conv-item__time">' + fmtTime(c.last_at) + '</div>'
+ '<button class="conv-item__delete" data-del-phone="' + escHtml(c.phone) + '" title="Borrar conversacion">&times;</button>'
+ '</div>';
});
// "Borrar todo" button at the bottom
html += '<div style="padding:8px;text-align:center;">'
+ '<button class="conv-delete-all" style="background:none;border:1px dashed var(--color-border,#444);color:var(--color-text-muted);padding:6px 12px;border-radius:6px;cursor:pointer;font-size:11px;" onclick="deleteAllConversations()">Borrar todas las conversaciones</button>'
+ '</div>';
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 = '<div class="conv-empty">Error cargando conversaciones</div>';
});
}
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 = '<div class="chat-empty">Error cargando mensajes: ' + escHtml(data.error) + '</div>';
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 = '<div class="chat-empty">Error de red al cargar mensajes</div>';
});
}
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 += '<div class="msg-bubble ' + cls + '">'
+ '<div class="msg-bubble__text">' + escHtml(text).replace(/\n/g, '<br>') + '</div>'
+ '<div class="msg-bubble__meta">' + fmtTime(time) + '</div>'
+ '</div>';
});
console.log('[WA-UI] renderMessages HTML length:', html.length);
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; }
// 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) {}
// Register Cmd+K items
if (typeof registerCmdKItem === "function") {
registerCmdKItem({ group: "Principal", label: "POS Ventas", href: "/pos/sale", icon: "🛒" });
registerCmdKItem({ group: "Principal", label: "Catálogo", href: "/pos/catalog", icon: "📁" });
registerCmdKItem({ group: "Principal", label: "Clientes", href: "/pos/customers", icon: "👤" });
registerCmdKItem({ group: "Principal", label: "Dashboard", href: "/pos/dashboard", icon: "📊" });
}
})();