/** * pos-utils.js — Shared utility functions for all POS pages. * * Provides common operations that multiple pages need: * - CSV export of any visible table * - Print page (PDF via browser print dialog) * - Toast notifications (if page doesn't have its own) * * Load this script in every POS template BEFORE page-specific JS. */ (function() { 'use strict'; // ── CSV Export ────────────────────────────────────────────────── // Finds the first visible on the page and downloads it as CSV. // Works on inventory, customers, invoicing, reports, accounting. window.exportVisibleTableCSV = function(prefix) { prefix = prefix || 'datos'; var tables = document.querySelectorAll('table'); var table = null; // Find first visible table with data rows for (var i = 0; i < tables.length; i++) { if (tables[i].offsetParent !== null && tables[i].querySelector('tbody tr')) { table = tables[i]; break; } } if (!table) { showToast('No hay tabla de datos para exportar en esta vista.', 'warn'); return; } var rows = []; // Header row var ths = table.querySelectorAll('thead th'); if (ths.length) { rows.push(Array.from(ths).map(function(th) { return '"' + th.textContent.trim().replace(/"/g, '""') + '"'; }).join(',')); } // Data rows table.querySelectorAll('tbody tr').forEach(function(tr) { var cells = tr.querySelectorAll('td'); rows.push(Array.from(cells).map(function(td) { return '"' + td.textContent.trim().replace(/"/g, '""') + '"'; }).join(',')); }); if (rows.length <= 1) { showToast('La tabla está vacía — no hay datos para exportar.', 'warn'); return; } var csv = rows.join('\n'); // BOM prefix so Excel opens UTF-8 correctly var blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = prefix + '_nexus_' + new Date().toISOString().slice(0, 10) + '.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('CSV descargado: ' + a.download, 'ok'); }; // ── Print (PDF) ──────────────────────────────────────────────── window.printPage = function() { window.print(); }; // ── Toast (simple, non-blocking notification) ────────────────── // Only creates its own toast if the page doesn't already have one. window.showToast = function(msg, type) { type = type || 'info'; var container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; container.style.cssText = 'position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none;'; document.body.appendChild(container); } var colors = { ok: 'background:#1a7a3a;color:#fff;', error: 'background:#c0392b;color:#fff;', warn: 'background:#d4a017;color:#000;', info: 'background:var(--color-surface-3,#333);color:var(--color-text-primary,#fff);', }; var toast = document.createElement('div'); toast.style.cssText = (colors[type] || colors.info) + 'padding:10px 20px;border-radius:8px;font-size:14px;font-weight:500;' + 'box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;' + 'animation:slideInRight 0.3s ease;max-width:400px;'; toast.textContent = msg; container.appendChild(toast); setTimeout(function() { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s'; setTimeout(function() { toast.remove(); }, 300); }, 3000); }; // ── "Próximamente" placeholder for features not yet built ────── window.featureProximamente = function(nombre) { showToast((nombre || 'Esta función') + ' estará disponible próximamente.', 'info'); }; // ── Table Filter Panel ──────────────────────────────────────── // Creates a dropdown filter panel that filters visible table rows // client-side. Call toggleFilterPanel(buttonEl, config) where config // is an array of {label, column, values} describing each filter. // // Usage (from onclick): // toggleFilterPanel(this, [ // {label: 'Marca', column: 2, values: ['BOSCH','MONROE','Todas']}, // {label: 'Status', column: 4, values: ['Activo','Inactivo','Todos']}, // ]) var _activeFilterPanel = null; window.toggleFilterPanel = function(btnEl, filters) { // Close existing panel if open if (_activeFilterPanel) { _activeFilterPanel.remove(); _activeFilterPanel = null; return; } var panel = document.createElement('div'); panel.className = 'filter-panel'; panel.style.cssText = 'position:absolute;top:100%;right:0;z-index:1000;' + 'background:var(--glass-bg-strong,#1a1a1a);backdrop-filter:blur(16px);' + 'border:1px solid var(--glass-border,#333);border-radius:var(--radius-lg,12px);' + 'padding:16px;min-width:260px;box-shadow:0 8px 32px rgba(0,0,0,0.3);' + 'display:flex;flex-direction:column;gap:12px;'; var title = document.createElement('div'); title.style.cssText = 'font-weight:700;font-size:14px;display:flex;justify-content:space-between;align-items:center;'; title.innerHTML = 'Filtros '; panel.appendChild(title); filters.forEach(function(f) { var group = document.createElement('div'); var label = document.createElement('label'); label.style.cssText = 'display:block;font-size:12px;color:var(--color-text-muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:0.05em;'; label.textContent = f.label; group.appendChild(label); var select = document.createElement('select'); select.style.cssText = 'width:100%;padding:8px 10px;background:var(--glass-bg,#222);' + 'border:1px solid var(--glass-border,#444);border-radius:6px;' + 'color:var(--color-text-primary,#fff);font-size:13px;'; select.dataset.filterColumn = f.column; // "Todos" option always first var allOpt = document.createElement('option'); allOpt.value = ''; allOpt.textContent = f.allLabel || 'Todos'; select.appendChild(allOpt); (f.values || []).forEach(function(v) { if (!v) return; var opt = document.createElement('option'); opt.value = v; opt.textContent = v; select.appendChild(opt); }); select.addEventListener('change', function() { applyFilters(panel); }); group.appendChild(select); panel.appendChild(group); }); // Clear all button var clearBtn = document.createElement('button'); clearBtn.style.cssText = 'padding:8px;background:transparent;border:1px dashed var(--glass-border,#444);' + 'border-radius:6px;color:var(--color-text-muted);cursor:pointer;font-size:12px;'; clearBtn.textContent = 'Limpiar filtros'; clearBtn.addEventListener('click', function() { panel.querySelectorAll('select').forEach(function(s) { s.value = ''; }); applyFilters(panel); }); panel.appendChild(clearBtn); // Position relative to the button var wrapper = btnEl.parentElement; if (wrapper) wrapper.style.position = 'relative'; (wrapper || document.body).appendChild(panel); _activeFilterPanel = panel; // Close on outside click setTimeout(function() { document.addEventListener('click', function handler(e) { if (!panel.contains(e.target) && e.target !== btnEl) { closeFilterPanel(); document.removeEventListener('click', handler); } }); }, 100); }; window.closeFilterPanel = function() { if (_activeFilterPanel) { _activeFilterPanel.remove(); _activeFilterPanel = null; } }; function applyFilters(panel) { var selects = panel.querySelectorAll('select[data-filter-column]'); // Find the nearest visible table var tables = document.querySelectorAll('table'); var table = null; for (var i = 0; i < tables.length; i++) { if (tables[i].offsetParent !== null) { table = tables[i]; break; } } if (!table) return; var rows = table.querySelectorAll('tbody tr'); rows.forEach(function(tr) { var show = true; selects.forEach(function(sel) { var col = parseInt(sel.dataset.filterColumn); var val = sel.value.toLowerCase(); if (!val) return; // "Todos" — no filter var cells = tr.querySelectorAll('td'); if (cells[col]) { var cellText = cells[col].textContent.trim().toLowerCase(); if (cellText.indexOf(val.toLowerCase()) === -1) show = false; } }); tr.style.display = show ? '' : 'none'; }); // Update count badge if exists var visibleCount = 0; rows.forEach(function(tr) { if (tr.style.display !== 'none') visibleCount++; }); var badge = document.querySelector('.filter-count-badge'); if (badge) badge.textContent = visibleCount + ' resultados'; } // ── Auto-extract unique values from a table column ────────── // Useful for building filter options dynamically from data. window.getUniqueColumnValues = function(tableEl, colIndex, maxValues) { maxValues = maxValues || 30; var values = {}; if (!tableEl) return []; tableEl.querySelectorAll('tbody tr').forEach(function(tr) { var cells = tr.querySelectorAll('td'); if (cells[colIndex]) { var v = cells[colIndex].textContent.trim(); if (v && v !== '-' && v !== '') values[v] = (values[v] || 0) + 1; } }); // Sort by frequency (most common first) return Object.keys(values) .sort(function(a, b) { return values[b] - values[a]; }) .slice(0, maxValues); }; // ── Auto-print polling for WhatsApp quotations ─────────────── // Polls /quotations/print-queue every 15s. When a confirmed WA quote // is found, it fetches the ESC/POS bytes and sends to the connected // thermal printer. Falls back to browser print if no thermal is connected. var _autoPrintTimer = null; var _autoPrintEnabled = false; window.startAutoPrint = function() { if (_autoPrintTimer) return; _autoPrintEnabled = true; var token = localStorage.getItem('pos_token'); if (!token) return; _autoPrintTimer = setInterval(function() { fetch('/pos/api/quotations/print-queue', { headers: { 'Authorization': 'Bearer ' + token } }) .then(function(r) { return r.json(); }) .then(function(d) { if (!d.data || !d.data.length) return; d.data.forEach(function(q) { console.log('[auto-print] Cotización #' + q.id + ' confirmada por WhatsApp — imprimiendo...'); showToast('🖨️ Imprimiendo cotización #' + q.id + ' (WhatsApp)', 'ok'); autoPrintQuote(q.id, token); }); }) .catch(function() {}); // silent on errors }, 15000); // every 15 seconds console.log('[auto-print] Enabled — polling every 15s'); }; window.stopAutoPrint = function() { if (_autoPrintTimer) { clearInterval(_autoPrintTimer); _autoPrintTimer = null; } _autoPrintEnabled = false; }; function autoPrintQuote(quoteId, token) { // Try thermal printer first (via NexusPrinter if loaded) if (typeof NexusPrinter !== 'undefined' && NexusPrinter.isConnected && NexusPrinter.isConnected()) { fetch('/pos/api/quotations/' + quoteId + '/print', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ printer_type: 'escpos_raw', width: 80 }), }) .then(function(r) { return r.arrayBuffer(); }) .then(function(buf) { NexusPrinter.sendRaw(new Uint8Array(buf)); markPrinted(quoteId, token); }) .catch(function(e) { console.error('[auto-print] Thermal print failed:', e); browserPrintQuote(quoteId, token); }); } else { browserPrintQuote(quoteId, token); } } function browserPrintQuote(quoteId, token) { // Fallback: open a print-friendly window fetch('/pos/api/quotations/' + quoteId + '/print', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ printer_type: 'browser' }), }) .then(function(r) { return r.json(); }) .then(function(q) { var html = 'Cotización #' + q.id + ''; html += ''; html += '

COTIZACIÓN

'; html += '

COT-' + q.id + '

'; html += '

Fecha: ' + (q.created_at || '').substring(0, 10) + '

'; if (q.customer_name) html += '

Cliente: ' + q.customer_name + '

'; if (q.wa_phone) html += '

WhatsApp: ' + q.wa_phone + '

'; html += '
'; (q.items || []).forEach(function(it) { html += ''; if (it.part_number) html += ''; }); html += '
' + it.quantity + 'x ' + it.name + '$' + it.subtotal.toFixed(2) + '
#' + it.part_number + '

'; html += '

Subtotal: $' + q.subtotal.toFixed(2) + '

'; html += '

IVA: $' + q.tax_total.toFixed(2) + '

'; html += '

TOTAL: $' + q.total.toFixed(2) + '

'; html += '

Esta cotización no es comprobante fiscal
Precios sujetos a disponibilidad

'; html += ''; var w = window.open('', '_blank', 'width=400,height=600'); w.document.write(html); w.document.close(); setTimeout(function() { w.print(); }, 500); markPrinted(quoteId, token); }) .catch(function(e) { console.error('[auto-print] Browser print failed:', e); }); } function markPrinted(quoteId, token) { fetch('/pos/api/quotations/' + quoteId + '/mark-printed', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, }).catch(function() {}); } // Auto-start polling on pages that are likely to have a printer // (POS sale page and quotations page) if (window.location.pathname.indexOf('/pos/sale') !== -1 || window.location.pathname.indexOf('/pos/quotation') !== -1 || window.location.pathname.indexOf('/pos/dashboard') !== -1) { var _initToken = localStorage.getItem('pos_token'); if (_initToken) { setTimeout(function() { startAutoPrint(); }, 3000); } } // Inject styles if (!document.getElementById('pos-utils-styles')) { var style = document.createElement('style'); style.id = 'pos-utils-styles'; style.textContent = '@keyframes slideInRight{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}' + '.filter-panel select:focus{outline:none;border-color:var(--color-primary,#F5A623);box-shadow:0 0 0 2px var(--glow-color-soft,rgba(245,166,35,0.15));}'; document.head.appendChild(style); } })();