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
This commit is contained in:
2026-05-06 20:27:14 +00:00
parent 371d72887e
commit ff45905b49
33 changed files with 3040 additions and 445 deletions

View File

@@ -1382,19 +1382,28 @@
});
function runSearch(q) {
apiFetch(API + '/search?q=' + encodeURIComponent(q) + '&limit=20').then(function (data) {
var url = API + '/search?q=' + encodeURIComponent(q) + '&limit=20';
if (nav.engine && nav.engine.id_mye) {
url += '&mye_id=' + nav.engine.id_mye;
}
apiFetch(url).then(function (data) {
if (!data || !data.data || !data.data.length) {
searchDropdown.innerHTML = '<div style="padding:var(--space-4);color:var(--color-text-muted);font-size:var(--text-body-sm);">Sin resultados para "' + esc(q) + '"</div>';
searchDropdown.classList.add('is-visible');
return;
}
searchDropdown.innerHTML = data.data.map(function (r) {
var isLocal = r.source === 'local_inventory' || (typeof r.id_part === 'string' && r.id_part.indexOf('inv:') === 0);
var stockLabel = r.local_stock > 0
? '<span class="stock-badge stock-badge--local" style="margin-left:auto;">Stock: ' + r.local_stock + '</span>'
: '';
return '<div class="search-result-item" data-part-id="' + r.id_part + '">' +
var localBadge = isLocal
? '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#4f46e5;">Stock Local</span>'
: '';
var oemNum = isLocal ? (r.oem_part_number || r.part_number || '') : (r.oem_part_number || '');
return '<div class="search-result-item" data-part-id="' + r.id_part + '" data-name="' + esc(r.name) + '" data-pn="' + esc(oemNum) + '" data-price="' + (r.local_price || '') + '" data-stock="' + (r.local_stock || 0) + '">' +
'<div style="flex:1;">' +
'<div class="search-result__oem">' + esc(r.oem_part_number) + '</div>' +
'<div class="search-result__oem">' + localBadge + esc(oemNum) + '</div>' +
'<div class="search-result__name">' + esc(r.name) + '</div>' +
(r.vehicle_info ? '<div class="search-result__vehicle">' + esc(r.vehicle_info) + '</div>' : '') +
'</div>' +
@@ -1408,6 +1417,12 @@
searchDropdown.classList.remove('is-visible');
var pid = this.dataset.partId;
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
var info = '💠 Stock Local\n\n' +
'Parte: ' + (this.dataset.pn || 'N/A') + '\n' +
'Nombre: ' + (this.dataset.name || '') + '\n' +
'Precio: $' + (this.dataset.price || '—') + '\n' +
'Stock: ' + (this.dataset.stock || 0) + ' pzas';
alert(info);
return;
}
openPartDetail(parseInt(pid));

View File

@@ -14,6 +14,16 @@
var currentSearch = '';
var draftCountId = null;
var inventoryVS = null;
var compatSource = 'both'; // default, loaded from config
// Load compatibility source setting
(function loadCompatSource() {
fetch('/pos/api/config/vehicle-compat-source', { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.source) compatSource = d.source;
}).catch(function() {});
})();
// --- API helper ---
function apiFetch(url, opts) {
@@ -695,7 +705,9 @@
} else {
html2 += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin vehiculos vinculados.</p>';
}
html2 += '<div style="margin-top:8px;"><button class="btn btn--primary btn--sm" onclick="autoMatchCompat(' + itemId + ')">Auto-Match por TecDoc</button> <span style="font-size:var(--text-caption);color:var(--color-text-muted);">Busca en catalogo central y vincula automaticamente</span></div>';
var btnLabel = compatSource === 'qwen' ? 'Auto-Match con IA (QWEN)' : (compatSource === 'both' ? 'Auto-Match (TecDoc + IA)' : 'Auto-Match por TecDoc');
var btnDesc = compatSource === 'qwen' ? 'Busca compatibilidad usando inteligencia artificial' : (compatSource === 'both' ? 'Busca en catalogo central y con IA' : 'Busca en catalogo central y vincula automaticamente');
html2 += '<div style="margin-top:8px;"><button class="btn btn--primary btn--sm" onclick="autoMatchCompat(' + itemId + ')">' + btnLabel + '</button> <span style="font-size:var(--text-caption);color:var(--color-text-muted);">' + btnDesc + '</span></div>';
el.innerHTML = html2;
})
.catch(function() {
@@ -735,7 +747,18 @@
headers: { 'Authorization': 'Bearer ' + token }
}).then(function(r) { return r.json(); })
.then(function(d) {
alert('Auto-match completado. Vehiculos vinculados: ' + (d.matched || 0));
var msg = '';
if (d.tecdoc && d.qwen) {
var t = d.tecdoc.matched ? (d.tecdoc.matched_count || d.tecdoc.matches ? d.tecdoc.matches.length : 0) : 0;
var q = d.qwen.total_qwen || 0;
var qi = d.qwen.inserted || 0;
msg = 'Auto-match completado.\nTecDoc: ' + t + ' vehiculos.\nIA QWEN: ' + qi + ' nuevos vinculados (de ' + q + ' encontrados).';
} else if (d.myes) {
msg = 'Auto-match completado. Vehiculos encontrados: ' + (d.total_qwen || d.myes.length) + ' (nuevos vinculados: ' + (d.inserted || 0) + ')';
} else {
msg = 'Auto-match completado. Vehiculos vinculados: ' + (d.matched ? 'Si' : 'No');
}
alert(msg);
viewProductDetail(itemId);
}).catch(function() { alert('Error en auto-match'); });
}

View File

@@ -82,8 +82,10 @@ const POS = (() => {
console.warn('Could not parse token:', e);
}
// Load cart from localStorage (from catalog)
// Load cart from localStorage (from catalog or quotation edit/convert)
const catalogCart = localStorage.getItem('pos_cart');
const editQuoteId = localStorage.getItem('pos_edit_quote_id');
const convertQuoteId = localStorage.getItem('pos_convert_quote_id');
if (catalogCart) {
try {
const items = JSON.parse(catalogCart);
@@ -93,6 +95,12 @@ const POS = (() => {
localStorage.removeItem('pos_cart');
} catch (e) { console.warn('Could not load catalog cart:', e); }
}
if (editQuoteId) {
showToast(`Modo edicion: Cotizacion #${editQuoteId}. Guardar actualizara la cotizacion.`);
}
if (convertQuoteId) {
showToast(`Modo conversion: Cotizacion #${convertQuoteId}. El pago convertira la cotizacion en venta.`);
}
// Load current register
await loadRegister();
@@ -702,10 +710,29 @@ const POS = (() => {
confirmBtn.textContent = 'Procesando...';
try {
const sale = await api('/pos/api/sales', {
method: 'POST',
body: JSON.stringify(saleData),
});
const convertQuoteId = localStorage.getItem('pos_convert_quote_id');
let sale;
if (convertQuoteId) {
const convertData = {
register_id: currentRegister ? currentRegister.id : null,
payment_method: paymentMethod,
sale_type: 'cash',
amount_paid: amountPaid,
payment_details: paymentDetails,
};
sale = await api('/pos/api/quotations/' + convertQuoteId + '/convert', {
method: 'POST',
body: JSON.stringify(convertData),
});
localStorage.removeItem('pos_convert_quote_id');
showToast(`Cotizacion #${convertQuoteId} convertida a venta #${sale.id}`);
} else {
sale = await api('/pos/api/sales', {
method: 'POST',
body: JSON.stringify(saleData),
});
showToast(`Venta #${sale.id} completada`);
}
lastSaleId = sale.id;
lastSaleData = sale;
@@ -717,8 +744,6 @@ const POS = (() => {
selectedRow = -1;
clearCustomer();
renderCart();
showToast(`Venta #${sale.id} completada`);
} catch (e) {
alert('Error al procesar venta: ' + e.message);
} finally {
@@ -790,12 +815,25 @@ const POS = (() => {
customer_id: currentCustomer ? currentCustomer.id : null,
};
const editQuoteId = localStorage.getItem('pos_edit_quote_id');
try {
const result = await api('/pos/api/quotations', {
method: 'POST',
body: JSON.stringify(body),
});
showToast(`Cotizacion #${result.id} guardada. Total: ${fmt(result.total)}`);
if (editQuoteId) {
const result = await api('/pos/api/quotations/' + editQuoteId, {
method: 'PUT',
body: JSON.stringify(body),
});
localStorage.removeItem('pos_edit_quote_id');
localStorage.removeItem('pos_edit_quote_customer_id');
localStorage.removeItem('pos_edit_quote_notes');
showToast(`Cotizacion #${editQuoteId} actualizada. Total: ${fmt(result.total)}`);
} else {
const result = await api('/pos/api/quotations', {
method: 'POST',
body: JSON.stringify(body),
});
showToast(`Cotizacion #${result.id} guardada. Total: ${fmt(result.total)}`);
}
} catch (e) {
alert('Error: ' + e.message);
}

View File

@@ -65,13 +65,13 @@
// -- DOM refs --------------------------------------------------------------
var convList = document.getElementById('convList');
var chatMessages = document.getElementById('chatMessages');
var chatMessages = document.getElementById('waChatMessages') || document.getElementById('chatMessages');
var chatHeader = document.getElementById('chatHeaderPhone');
var chatInput = document.getElementById('chatInput');
var sendBtn = document.getElementById('sendBtn');
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('chatPanel');
var chatPanel = document.getElementById('waChatPanel') || document.getElementById('chatPanel');
var statusDot = document.getElementById('statusDot');
var statusText = document.getElementById('statusText');
var connectSection = document.getElementById('connectSection');
@@ -275,6 +275,7 @@
activePhone = null;
chatPanel.style.display = 'none';
emptyState.style.display = '';
if (messengerArea) messengerArea.classList.remove('has-active-chat');
}
loadConversations();
} else {
@@ -300,42 +301,65 @@
var activeContactName = '';
function openConversation(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';
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);
});
convList.querySelectorAll('.conv-item').forEach(function (el) {
el.classList.toggle('is-active', el.getAttribute('data-phone') === phone);
});
loadMessages(phone);
startPolling();
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';
// Support both 'text' and 'message_text' keys (backend changed)
var text = m.message_text || m.text || '';
// Support both 'created_at' and 'date' keys
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;
}

489
pos/static/js/whatsapp2.js Normal file
View File

@@ -0,0 +1,489 @@
/**
* 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) {}
})();