603 lines
28 KiB
JavaScript
603 lines
28 KiB
JavaScript
/**
|
|
* 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 <table> 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 (enhanced with icons, progress bar, close button, actions) ──
|
|
var _toastIcons = {
|
|
ok: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>',
|
|
error: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
|
|
warn: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
|
info: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>'
|
|
};
|
|
var _toastTitles = { ok: 'Éxito', error: 'Error', warn: 'Advertencia', info: 'Información' };
|
|
|
|
window.showToast = function(msg, type, opts) {
|
|
type = type || 'info';
|
|
opts = opts || {};
|
|
var container = document.getElementById('toast-container');
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.id = 'toast-container';
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
var toast = document.createElement('div');
|
|
toast.className = 'toast toast--' + type;
|
|
|
|
var iconHtml = '<div class="toast__icon">' + (_toastIcons[type] || _toastIcons.info) + '</div>';
|
|
var titleHtml = opts.title ? '<div class="toast__title">' + opts.title + '</div>' : '';
|
|
var actionHtml = '';
|
|
if (opts.action && opts.action.text) {
|
|
actionHtml = '<div class="toast__action"><button onclick="this.closest(\'.toast\').__toastAction()">' + opts.action.text + '</button></div>';
|
|
toast.__toastAction = function() {
|
|
if (opts.action.callback) opts.action.callback();
|
|
_removeToast(toast);
|
|
};
|
|
}
|
|
var progressHtml = '<div class="toast__progress" style="animation-duration:' + (opts.duration || 4000) + 'ms;"></div>';
|
|
|
|
toast.innerHTML = iconHtml +
|
|
'<div class="toast__content">' + titleHtml + '<div class="toast__msg">' + msg + '</div>' + actionHtml + '</div>' +
|
|
'<button class="toast__close" onclick="_removeToast(this.closest(\'.toast\'))">✕</button>' +
|
|
progressHtml;
|
|
|
|
container.appendChild(toast);
|
|
|
|
var timer = setTimeout(function() { _removeToast(toast); }, opts.duration || 4000);
|
|
toast.__toastTimer = timer;
|
|
|
|
toast.addEventListener('mouseenter', function() { clearTimeout(timer); var p = toast.querySelector('.toast__progress'); if (p) p.style.animationPlayState = 'paused'; });
|
|
toast.addEventListener('mouseleave', function() { var p = toast.querySelector('.toast__progress'); if (p) p.style.animationPlayState = 'running'; timer = setTimeout(function() { _removeToast(toast); }, 2000); toast.__toastTimer = timer; });
|
|
};
|
|
|
|
window._removeToast = function(toast) {
|
|
if (!toast || toast.__toastRemoved) return;
|
|
toast.__toastRemoved = true;
|
|
if (toast.__toastTimer) clearTimeout(toast.__toastTimer);
|
|
toast.style.animation = 'toastSlideOut 0.25s ease forwards';
|
|
setTimeout(function() { toast.remove(); }, 260);
|
|
};
|
|
|
|
// ── Skeleton helpers ──────────────────────────────────────────
|
|
window.renderSkeletonRows = function(cols, rows) {
|
|
rows = rows || 6;
|
|
var html = '';
|
|
for (var i = 0; i < rows; i++) {
|
|
html += '<tr class="skeleton--table-row">';
|
|
for (var j = 0; j < cols; j++) {
|
|
html += '<td><div class="skeleton"></div></td>';
|
|
}
|
|
html += '</tr>';
|
|
}
|
|
return html;
|
|
};
|
|
|
|
window.showSkeleton = function(containerSelector, cols, rows) {
|
|
var el = typeof containerSelector === 'string' ? document.querySelector(containerSelector) : containerSelector;
|
|
if (!el) return;
|
|
el.dataset.originalContent = el.innerHTML;
|
|
el.innerHTML = renderSkeletonRows(cols || 6, rows || 6);
|
|
};
|
|
|
|
window.hideSkeleton = function(containerSelector) {
|
|
var el = typeof containerSelector === 'string' ? document.querySelector(containerSelector) : containerSelector;
|
|
if (!el || el.dataset.originalContent === undefined) return;
|
|
el.innerHTML = el.dataset.originalContent;
|
|
delete el.dataset.originalContent;
|
|
};
|
|
|
|
// ── Empty state helper ────────────────────────────────────────
|
|
window.renderEmptyState = function(opts) {
|
|
opts = opts || {};
|
|
var icon = opts.icon || '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="9" y1="9" x2="15" y2="15"/><line x1="15" y1="9" x2="9" y2="15"/></svg>';
|
|
var title = opts.title || 'Sin datos';
|
|
var subtitle = opts.subtitle || 'No hay información disponible en este momento.';
|
|
var action = opts.action ? '<div class="empty-state__action">' + opts.action + '</div>' : '';
|
|
return '<div class="empty-state">' +
|
|
'<div class="empty-state__icon">' + icon + '</div>' +
|
|
'<div class="empty-state__title">' + title + '</div>' +
|
|
'<div class="empty-state__subtitle">' + subtitle + '</div>' +
|
|
action + '</div>';
|
|
};
|
|
|
|
// ── Cmd+K Global Search ───────────────────────────────────────
|
|
(function() {
|
|
var cmdkOverlay = null, cmdkInput = null, cmdkResults = null, cmdkSelected = -1;
|
|
var cmdkItems = [];
|
|
|
|
function buildCmdK() {
|
|
if (cmdkOverlay) return;
|
|
cmdkOverlay = document.createElement('div');
|
|
cmdkOverlay.className = 'cmdk-overlay';
|
|
cmdkOverlay.innerHTML =
|
|
'<div class="cmdk-modal" role="dialog" aria-label="Búsqueda global">' +
|
|
' <div class="cmdk-input-wrap">' +
|
|
' <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--color-text-muted);flex-shrink:0;"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' +
|
|
' <input type="text" class="cmdk-input" placeholder="Buscar módulos, productos, clientes..." autocomplete="off">' +
|
|
' <span class="cmdk-shortcut">ESC</span>' +
|
|
' </div>' +
|
|
' <div class="cmdk-results"></div>' +
|
|
' <div class="cmdk-footer"><span>↑↓ navegar · ↵ seleccionar</span><span>' + cmdkItems.length + ' resultados</span></div>' +
|
|
'</div>';
|
|
document.body.appendChild(cmdkOverlay);
|
|
cmdkInput = cmdkOverlay.querySelector('.cmdk-input');
|
|
cmdkResults = cmdkOverlay.querySelector('.cmdk-results');
|
|
|
|
cmdkOverlay.addEventListener('click', function(e) { if (e.target === cmdkOverlay) closeCmdK(); });
|
|
cmdkInput.addEventListener('input', function() { filterCmdK(this.value); });
|
|
cmdkInput.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') { closeCmdK(); return; }
|
|
if (e.key === 'ArrowDown') { e.preventDefault(); moveCmdK(1); }
|
|
if (e.key === 'ArrowUp') { e.preventDefault(); moveCmdK(-1); }
|
|
if (e.key === 'Enter') { e.preventDefault(); activateCmdK(); }
|
|
});
|
|
}
|
|
|
|
function openCmdK() {
|
|
buildCmdK();
|
|
cmdkOverlay.classList.add('is-open');
|
|
cmdkInput.value = '';
|
|
cmdkInput.focus();
|
|
filterCmdK('');
|
|
}
|
|
|
|
function closeCmdK() {
|
|
if (cmdkOverlay) cmdkOverlay.classList.remove('is-open');
|
|
}
|
|
|
|
function moveCmdK(dir) {
|
|
var items = cmdkResults.querySelectorAll('.cmdk-item');
|
|
if (!items.length) return;
|
|
cmdkSelected += dir;
|
|
if (cmdkSelected < 0) cmdkSelected = items.length - 1;
|
|
if (cmdkSelected >= items.length) cmdkSelected = 0;
|
|
items.forEach(function(it, i) { it.classList.toggle('is-selected', i === cmdkSelected); });
|
|
var sel = items[cmdkSelected];
|
|
if (sel) sel.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
|
|
function activateCmdK() {
|
|
var items = cmdkResults.querySelectorAll('.cmdk-item');
|
|
var sel = items[cmdkSelected];
|
|
if (sel && sel.dataset.href) { closeCmdK(); window.location.href = sel.dataset.href; }
|
|
}
|
|
|
|
function filterCmdK(q) {
|
|
q = (q || '').toLowerCase().trim();
|
|
var groups = {};
|
|
cmdkItems.forEach(function(item) {
|
|
if (!q || item.label.toLowerCase().indexOf(q) !== -1 || (item.keywords || '').toLowerCase().indexOf(q) !== -1) {
|
|
groups[item.group] = groups[item.group] || [];
|
|
groups[item.group].push(item);
|
|
}
|
|
});
|
|
var html = '';
|
|
var total = 0;
|
|
Object.keys(groups).forEach(function(g) {
|
|
html += '<div class="cmdk-group"><div class="cmdk-group__label">' + g + '</div>';
|
|
groups[g].forEach(function(item) {
|
|
total++;
|
|
html += '<div class="cmdk-item" data-href="' + (item.href || '') + '">' +
|
|
'<div class="cmdk-item__icon">' + (item.icon || '→') + '</div>' +
|
|
'<div>' + item.label + '</div>' +
|
|
(item.meta ? '<div class="cmdk-item__meta">' + item.meta + '</div>' : '') +
|
|
'</div>';
|
|
});
|
|
html += '</div>';
|
|
});
|
|
if (!total) html = '<div style="padding:24px;text-align:center;color:var(--color-text-muted);">Sin resultados</div>';
|
|
cmdkResults.innerHTML = html;
|
|
cmdkSelected = 0;
|
|
var first = cmdkResults.querySelector('.cmdk-item');
|
|
if (first) first.classList.add('is-selected');
|
|
var footer = cmdkOverlay.querySelector('.cmdk-footer span:last-child');
|
|
if (footer) footer.textContent = total + ' resultados';
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openCmdK(); }
|
|
});
|
|
|
|
window.registerCmdKItem = function(item) {
|
|
if (!item || !item.label) return;
|
|
cmdkItems.push(item);
|
|
};
|
|
|
|
window.openCmdK = openCmdK;
|
|
window.closeCmdK = closeCmdK;
|
|
})();
|
|
|
|
// ── Connection indicator helper ───────────────────────────────
|
|
window.ConnectionStatus = {
|
|
online: function() { return navigator.onLine; },
|
|
render: function(containerId) {
|
|
var el = document.getElementById(containerId);
|
|
if (!el) return;
|
|
function update() {
|
|
var isOnline = navigator.onLine;
|
|
el.className = 'connection-indicator' + (isOnline ? '' : ' connection-indicator--offline');
|
|
el.innerHTML = '<span></span>' + (isOnline ? 'En línea' : 'Sin conexión');
|
|
}
|
|
update();
|
|
window.addEventListener('online', update);
|
|
window.addEventListener('offline', update);
|
|
}
|
|
};
|
|
|
|
// ── Bulk toolbar helper ───────────────────────────────────────
|
|
window.renderBulkToolbar = function(count, actionsHtml) {
|
|
return '<div class="bulk-toolbar">' +
|
|
'<div class="bulk-toolbar__count">' + count + ' seleccionado' + (count !== 1 ? 's' : '') + '</div>' +
|
|
'<div class="bulk-toolbar__actions">' + actionsHtml + '</div>' +
|
|
'</div>';
|
|
};
|
|
|
|
// ── Entrance animation helper ─────────────────────────────────
|
|
window.animateEntrance = function(selector, animClass, stagger) {
|
|
animClass = animClass || 'animate-fade-in-up';
|
|
stagger = stagger || 0.05;
|
|
var els = document.querySelectorAll(selector);
|
|
els.forEach(function(el, i) {
|
|
el.style.animationDelay = (i * stagger) + 's';
|
|
el.classList.add(animClass);
|
|
});
|
|
};
|
|
|
|
// ── "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 <button onclick="closeFilterPanel()" style="background:none;border:none;color:var(--color-text-muted);cursor:pointer;font-size:18px;">✕</button>';
|
|
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 = '<html><head><title>Cotización #' + q.id + '</title>';
|
|
html += '<style>body{font-family:monospace;font-size:12px;width:80mm;margin:0 auto;padding:10px;}';
|
|
html += 'h1{font-size:18px;text-align:center;margin:0;}';
|
|
html += '.center{text-align:center;}.right{text-align:right;}';
|
|
html += 'hr{border:none;border-top:1px dashed #000;}';
|
|
html += 'table{width:100%;border-collapse:collapse;}td{padding:2px 4px;}</style></head><body>';
|
|
html += '<h1>COTIZACIÓN</h1>';
|
|
html += '<p class="center">COT-' + q.id + '</p>';
|
|
html += '<p>Fecha: ' + (q.created_at || '').substring(0, 10) + '</p>';
|
|
if (q.customer_name) html += '<p>Cliente: ' + q.customer_name + '</p>';
|
|
if (q.wa_phone) html += '<p>WhatsApp: ' + q.wa_phone + '</p>';
|
|
html += '<hr><table>';
|
|
(q.items || []).forEach(function(it) {
|
|
html += '<tr><td>' + it.quantity + 'x ' + it.name + '</td><td class="right">$' + it.subtotal.toFixed(2) + '</td></tr>';
|
|
if (it.part_number) html += '<tr><td colspan="2" style="font-size:10px;color:#666;"> #' + it.part_number + '</td></tr>';
|
|
});
|
|
html += '</table><hr>';
|
|
html += '<p class="right">Subtotal: $' + q.subtotal.toFixed(2) + '</p>';
|
|
html += '<p class="right">IVA: $' + q.tax_total.toFixed(2) + '</p>';
|
|
html += '<p class="right" style="font-size:16px;font-weight:bold;">TOTAL: $' + q.total.toFixed(2) + '</p>';
|
|
html += '<hr><p class="center" style="font-size:10px;">Esta cotización no es comprobante fiscal<br>Precios sujetos a disponibilidad</p>';
|
|
html += '</body></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);
|
|
}
|
|
}
|
|
|
|
})();
|