Files
Autoparts-DB/pos/static/js/whatsapp2.js
consultoria-as ff45905b49 feat(whatsapp): QWEN primary AI backend, Hermes fallback, conversation history, vehicle persistence, demo prompts
- Add QWEN (qwen3.6) as primary AI backend with short system prompt
- Hermes remains as fallback with 45s timeout
- Increase QWEN timeout to 35s, max_tokens to 4000
- Add conversation history loading from whatsapp_messages (last 4 msgs)
- Persist detected vehicle in whatsapp_sessions table
- Add 'limpiar chat' / 'nuevo chat' / 'reset' commands to clear history
- Fix CSS conflict: rename whatsapp chat-panel classes to wa-chat-panel
- Fix JS ID conflicts with chat.js widget (waChatPanel, waChatMessages, etc.)
- Improve no-stock response: conversational with alternatives
- Split search_query by | for multi-part lookups
- Add DEMO_PROMPTS.md and DEMO_PROMPTS_V2.md
2026-05-06 20:27:14 +00:00

490 lines
18 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 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 = '<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) {}
})();