/**
* 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 += '