feat(ui): helpers pos-utils.js (barcode feedback, saved filters, resizable columns, density/touch toggles, notifications dropdown, ticket preview, image comparator, infinite scroll, sparklines) + inventory.html modals + pos-ui.css timeline & kanban
This commit is contained in:
@@ -5,6 +5,40 @@
|
|||||||
* Load AFTER tokens.css and common.css, BEFORE page-specific CSS.
|
* Load AFTER tokens.css and common.css, BEFORE page-specific CSS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════
|
||||||
|
0. ICON BUTTONS (header bar)
|
||||||
|
═══════════════════════════════════════════════════════════════ */
|
||||||
|
.icon-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: var(--radius-md, 8px);
|
||||||
|
background: var(--color-surface-2, #222);
|
||||||
|
border: 1px solid var(--color-border, #2a2a2a);
|
||||||
|
color: var(--color-text-secondary, #aaa);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.icon-btn:hover {
|
||||||
|
background: var(--color-surface-3, #333);
|
||||||
|
color: var(--color-text-primary, #eee);
|
||||||
|
border-color: var(--color-primary, #F5A623);
|
||||||
|
}
|
||||||
|
.notif-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-error, #ef4444);
|
||||||
|
box-shadow: 0 0 4px var(--color-error, #ef4444);
|
||||||
|
}
|
||||||
|
.notif-dot:empty { display: none; }
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════════════════
|
||||||
1. CUSTOM SCROLLBAR (global)
|
1. CUSTOM SCROLLBAR (global)
|
||||||
═══════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════ */
|
||||||
@@ -720,6 +754,36 @@ input:disabled, select:disabled, textarea:disabled {
|
|||||||
.nx-loader--sm .nx-loader__ring:nth-child(2) { inset: 4px; }
|
.nx-loader--sm .nx-loader__ring:nth-child(2) { inset: 4px; }
|
||||||
.nx-loader--sm .nx-loader__ring:nth-child(3) { inset: 8px; }
|
.nx-loader--sm .nx-loader__ring:nth-child(3) { inset: 8px; }
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════
|
||||||
|
21. TIMELINE
|
||||||
|
═══════════════════════════════════════════════════════════════ */
|
||||||
|
.timeline { position: relative; padding-left: 24px; }
|
||||||
|
.timeline::before { content: ''; position: absolute; left: 7px; top: 4px; bottom: 4px; width: 2px; background: var(--color-border, #2a2a2a); }
|
||||||
|
.timeline__item { position: relative; margin-bottom: var(--space-4, 1rem); display: flex; gap: 12px; align-items: flex-start; }
|
||||||
|
.timeline__dot { width: 14px; height: 14px; border-radius: 50%; background: var(--color-primary, #F5A623); border: 3px solid var(--color-surface-1, #1a1a1a); flex-shrink: 0; margin-left: -24px; margin-top: 3px; z-index: 1; }
|
||||||
|
.timeline__dot--green { background: var(--color-success, #22c55e); }
|
||||||
|
.timeline__dot--red { background: var(--color-error, #ef4444); }
|
||||||
|
.timeline__dot--blue { background: #3b82f6; }
|
||||||
|
.timeline__content { flex: 1; }
|
||||||
|
.timeline__date { font-size: 11px; color: var(--color-text-muted, #888); margin-bottom: 2px; }
|
||||||
|
.timeline__title { font-size: 13px; font-weight: 600; color: var(--color-text-primary, #eee); }
|
||||||
|
.timeline__desc { font-size: 12px; color: var(--color-text-secondary, #aaa); margin-top: 2px; }
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════
|
||||||
|
22. KANBAN BOARD
|
||||||
|
═══════════════════════════════════════════════════════════════ */
|
||||||
|
.kanban { display: flex; gap: var(--space-4, 1rem); overflow-x: auto; padding-bottom: var(--space-2, 0.5rem); }
|
||||||
|
.kanban__col { min-width: 280px; max-width: 320px; flex: 1; background: var(--color-surface-2, #222); border-radius: var(--radius-lg, 12px); border: 1px solid var(--color-border, #2a2a2a); display: flex; flex-direction: column; max-height: 70vh; }
|
||||||
|
.kanban__col-header { padding: 12px 16px; border-bottom: 1px solid var(--color-border, #2a2a2a); font-size: 13px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.kanban__col-count { font-size: 11px; padding: 2px 8px; border-radius: var(--radius-full, 999px); background: var(--color-surface-3, #333); color: var(--color-text-muted, #888); }
|
||||||
|
.kanban__cards { flex: 1; overflow-y: auto; padding: var(--space-3, 0.75rem); display: flex; flex-direction: column; gap: var(--space-2, 0.5rem); }
|
||||||
|
.kanban__card { background: var(--color-surface-1, #1a1a1a); border: 1px solid var(--color-border, #2a2a2a); border-radius: var(--radius-md, 8px); padding: 12px; cursor: grab; transition: all 0.15s; }
|
||||||
|
.kanban__card:hover { border-color: var(--color-primary, #F5A623); transform: translateY(-1px); }
|
||||||
|
.kanban__card-title { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
|
||||||
|
.kanban__card-meta { font-size: 11px; color: var(--color-text-muted, #888); }
|
||||||
|
.kanban__card.dragging { opacity: 0.5; cursor: grabbing; }
|
||||||
|
.kanban__col.drag-over { background: var(--color-surface-3, #333); border-color: var(--color-primary, #F5A623); }
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════════════════
|
||||||
25. SAVED FILTERS CHIPS
|
25. SAVED FILTERS CHIPS
|
||||||
═══════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════ */
|
||||||
|
|||||||
@@ -250,6 +250,10 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
inventoryVS.setData(items);
|
inventoryVS.setData(items);
|
||||||
|
// Make columns resizable
|
||||||
|
if (typeof makeTableResizable === 'function') {
|
||||||
|
makeTableResizable('#stockTable');
|
||||||
|
}
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
var pg = data.pagination || {};
|
var pg = data.pagination || {};
|
||||||
|
|||||||
@@ -599,4 +599,275 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Barcode Scanner Feedback ──────────────────────────────────
|
||||||
|
window.BarcodeFeedback = {
|
||||||
|
_audioCtx: null,
|
||||||
|
success: function() {
|
||||||
|
this._beep(800, 0.1, 'sine');
|
||||||
|
this._flash('#22c55e');
|
||||||
|
if (navigator.vibrate) navigator.vibrate(50);
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
this._beep(200, 0.15, 'square');
|
||||||
|
this._flash('#ef4444');
|
||||||
|
if (navigator.vibrate) navigator.vibrate([80, 50, 80]);
|
||||||
|
},
|
||||||
|
_beep: function(freq, duration, type) {
|
||||||
|
try {
|
||||||
|
if (!this._audioCtx) this._audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
var osc = this._audioCtx.createOscillator();
|
||||||
|
var gain = this._audioCtx.createGain();
|
||||||
|
osc.type = type || 'sine';
|
||||||
|
osc.frequency.value = freq;
|
||||||
|
gain.gain.value = 0.1;
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this._audioCtx.destination);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(this._audioCtx.currentTime + duration);
|
||||||
|
} catch(e) {}
|
||||||
|
},
|
||||||
|
_flash: function(color) {
|
||||||
|
var el = document.createElement('div');
|
||||||
|
el.style.cssText = 'position:fixed;inset:0;z-index:99999;opacity:0.3;background:' + color + ';pointer-events:none;transition:opacity 0.2s;';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
setTimeout(function() { el.style.opacity = '0'; }, 50);
|
||||||
|
setTimeout(function() { el.remove(); }, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Saved Filters ─────────────────────────────────────────────
|
||||||
|
window.SavedFilters = {
|
||||||
|
_key: function(page) { return 'pos_filters_' + (page || window.location.pathname); },
|
||||||
|
save: function(name, filters) {
|
||||||
|
var key = this._key();
|
||||||
|
var saved = this.list();
|
||||||
|
saved.push({ name: name, filters: filters, created: Date.now() });
|
||||||
|
localStorage.setItem(key, JSON.stringify(saved));
|
||||||
|
},
|
||||||
|
list: function() {
|
||||||
|
try { return JSON.parse(localStorage.getItem(this._key()) || '[]'); } catch(e) { return []; }
|
||||||
|
},
|
||||||
|
remove: function(name) {
|
||||||
|
var saved = this.list().filter(function(f) { return f.name !== name; });
|
||||||
|
localStorage.setItem(this._key(), JSON.stringify(saved));
|
||||||
|
},
|
||||||
|
renderChips: function(containerId, onApply) {
|
||||||
|
var container = document.getElementById(containerId);
|
||||||
|
if (!container) return;
|
||||||
|
var saved = this.list();
|
||||||
|
if (!saved.length) { container.innerHTML = ''; return; }
|
||||||
|
var html = '';
|
||||||
|
saved.forEach(function(f) {
|
||||||
|
html += '<span class="filter-chip">' + esc(f.name) +
|
||||||
|
'<button class="filter-chip__remove" onclick="SavedFilters.remove(\'' + esc(f.name) + '\');SavedFilters.renderChips(\'' + containerId + '\');">×</button></span>';
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
container.querySelectorAll('.filter-chip').forEach(function(chip, i) {
|
||||||
|
chip.addEventListener('click', function(e) {
|
||||||
|
if (e.target.classList.contains('filter-chip__remove')) return;
|
||||||
|
if (onApply) onApply(saved[i].filters);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Resizable Columns ─────────────────────────────────────────
|
||||||
|
window.makeTableResizable = function(tableSelector) {
|
||||||
|
var table = document.querySelector(tableSelector);
|
||||||
|
if (!table) return;
|
||||||
|
var ths = table.querySelectorAll('thead th');
|
||||||
|
ths.forEach(function(th, i) {
|
||||||
|
if (i >= ths.length - 1) return; // skip last column
|
||||||
|
var handle = document.createElement('div');
|
||||||
|
handle.className = 'resize-handle';
|
||||||
|
th.appendChild(handle);
|
||||||
|
|
||||||
|
handle.addEventListener('mousedown', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var startX = e.pageX;
|
||||||
|
var startW = th.offsetWidth;
|
||||||
|
th.classList.add('is-resizing');
|
||||||
|
|
||||||
|
function onMove(ev) {
|
||||||
|
var newW = Math.max(60, startW + (ev.pageX - startX));
|
||||||
|
th.style.width = newW + 'px';
|
||||||
|
th.style.minWidth = newW + 'px';
|
||||||
|
}
|
||||||
|
function onUp() {
|
||||||
|
th.classList.remove('is-resizing');
|
||||||
|
document.removeEventListener('mousemove', onMove);
|
||||||
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
}
|
||||||
|
document.addEventListener('mousemove', onMove);
|
||||||
|
document.addEventListener('mouseup', onUp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Density / Touch Mode Toggles ──────────────────────────────
|
||||||
|
window.DensityToggle = {
|
||||||
|
set: function(density) {
|
||||||
|
document.documentElement.setAttribute('data-density', density);
|
||||||
|
localStorage.setItem('pos_density', density);
|
||||||
|
},
|
||||||
|
init: function() {
|
||||||
|
var saved = localStorage.getItem('pos_density') || 'normal';
|
||||||
|
document.documentElement.setAttribute('data-density', saved);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.TouchModeToggle = {
|
||||||
|
set: function(enabled) {
|
||||||
|
document.documentElement.setAttribute('data-touch', enabled ? 'true' : 'false');
|
||||||
|
localStorage.setItem('pos_touch_mode', enabled ? 'true' : 'false');
|
||||||
|
},
|
||||||
|
init: function() {
|
||||||
|
var saved = localStorage.getItem('pos_touch_mode') === 'true';
|
||||||
|
document.documentElement.setAttribute('data-touch', saved ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
DensityToggle.init();
|
||||||
|
TouchModeToggle.init();
|
||||||
|
|
||||||
|
// ── Notifications Dropdown (functional) ───────────────────────
|
||||||
|
window.NotificationsDropdown = {
|
||||||
|
_visible: false,
|
||||||
|
_el: null,
|
||||||
|
toggle: function() {
|
||||||
|
if (this._visible) { this.hide(); return; }
|
||||||
|
this.show();
|
||||||
|
},
|
||||||
|
show: function() {
|
||||||
|
if (this._el) this._el.remove();
|
||||||
|
var btn = document.getElementById('notifDropdownBtn');
|
||||||
|
var el = document.createElement('div');
|
||||||
|
el.className = 'notif-dropdown';
|
||||||
|
el.innerHTML = '<div class="notif-dropdown__header">Notificaciones <button onclick="NotificationsDropdown.hide()" style="background:none;border:none;color:var(--color-text-muted);cursor:pointer;font-size:16px;">✕</button></div>' +
|
||||||
|
'<div class="notif-dropdown__list" id="notifDropdownList"><div class="notif-dropdown__empty">Sin notificaciones nuevas</div></div>';
|
||||||
|
if (btn) {
|
||||||
|
btn.parentElement.style.position = 'relative';
|
||||||
|
btn.parentElement.appendChild(el);
|
||||||
|
} else {
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}
|
||||||
|
this._el = el;
|
||||||
|
this._visible = true;
|
||||||
|
this._load();
|
||||||
|
setTimeout(function() {
|
||||||
|
document.addEventListener('click', function handler(e) {
|
||||||
|
if (!el.contains(e.target) && e.target !== btn) {
|
||||||
|
NotificationsDropdown.hide();
|
||||||
|
document.removeEventListener('click', handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
hide: function() { if (this._el) { this._el.remove(); this._el = null; } this._visible = false; },
|
||||||
|
_load: function() {
|
||||||
|
// Stub: can be wired to /api/notifications endpoint
|
||||||
|
var list = document.getElementById('notifDropdownList');
|
||||||
|
if (!list) return;
|
||||||
|
// Example: show inventory alerts count if available
|
||||||
|
var token = localStorage.getItem('pos_token');
|
||||||
|
if (!token) return;
|
||||||
|
fetch('/pos/api/inventory/alerts', { headers: { 'Authorization': 'Bearer ' + token } })
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
var count = (d.counts || {}).critical || 0;
|
||||||
|
if (count > 0) {
|
||||||
|
list.innerHTML = '<div class="notif-dropdown__item notif-dropdown__item--unread" onclick="window.location.href=\'/pos/inventory#alertas\'">' +
|
||||||
|
'<div class="notif-dropdown__icon">⚠️</div>' +
|
||||||
|
'<div class="notif-dropdown__content"><div class="notif-dropdown__title">' + count + ' producto' + (count > 1 ? 's' : '') + ' sin stock</div>' +
|
||||||
|
'<div class="notif-dropdown__time">Stock crítico</div></div></div>';
|
||||||
|
}
|
||||||
|
}).catch(function() {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Ticket Preview Helper ─────────────────────────────────────
|
||||||
|
window.previewTicket = function(ticketData) {
|
||||||
|
var data = ticketData || {};
|
||||||
|
var items = (data.items || []).map(function(it) {
|
||||||
|
return '<div class="ticket-preview__row"><span>' + (it.quantity || 1) + 'x ' + esc(it.name) + '</span><span>$' + (it.subtotal || 0).toFixed(2) + '</span></div>';
|
||||||
|
}).join('');
|
||||||
|
var html = '<div class="ticket-preview">' +
|
||||||
|
'<div class="ticket-preview__header"><div class="ticket-preview__title">Nexus Autoparts</div><div class="ticket-preview__meta">' + (data.store || 'Sucursal Centro') + '</div></div>' +
|
||||||
|
'<div class="ticket-preview__meta" style="text-align:center;margin-bottom:8px;">' + new Date().toLocaleString('es-MX') + '</div>' +
|
||||||
|
'<div class="ticket-preview__row"><span>Ticket #' + (data.id || '---') + '</span></div>' +
|
||||||
|
'<hr style="border:none;border-top:1px dashed #ccc;margin:8px 0;">' +
|
||||||
|
items +
|
||||||
|
'<hr style="border:none;border-top:1px dashed #ccc;margin:8px 0;">' +
|
||||||
|
'<div class="ticket-preview__row"><span>Subtotal</span><span>$' + (data.subtotal || 0).toFixed(2) + '</span></div>' +
|
||||||
|
'<div class="ticket-preview__row"><span>IVA</span><span>$' + (data.tax || 0).toFixed(2) + '</span></div>' +
|
||||||
|
'<div class="ticket-preview__total"><span>TOTAL</span><span>$' + (data.total || 0).toFixed(2) + '</span></div>' +
|
||||||
|
'<div style="text-align:center;font-size:10px;color:#666;margin-top:12px;">Gracias por su compra</div>' +
|
||||||
|
'</div>';
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Image Comparator Helper ───────────────────────────────────
|
||||||
|
window.initImageComparator = function(containerSelector) {
|
||||||
|
var container = document.querySelector(containerSelector);
|
||||||
|
if (!container) return;
|
||||||
|
var overlay = container.querySelector('.img-compare__overlay');
|
||||||
|
var handle = container.querySelector('.img-compare__handle');
|
||||||
|
if (!overlay || !handle) return;
|
||||||
|
|
||||||
|
function move(x) {
|
||||||
|
var rect = container.getBoundingClientRect();
|
||||||
|
var pct = Math.max(0, Math.min(100, ((x - rect.left) / rect.width) * 100));
|
||||||
|
overlay.style.width = pct + '%';
|
||||||
|
handle.style.left = pct + '%';
|
||||||
|
}
|
||||||
|
handle.addEventListener('mousedown', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
function onMove(ev) { move(ev.pageX); }
|
||||||
|
function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }
|
||||||
|
document.addEventListener('mousemove', onMove);
|
||||||
|
document.addEventListener('mouseup', onUp);
|
||||||
|
});
|
||||||
|
handle.addEventListener('touchstart', function(e) {
|
||||||
|
function onMove(ev) { move(ev.touches[0].pageX); }
|
||||||
|
function onUp() { document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onUp); }
|
||||||
|
document.addEventListener('touchmove', onMove);
|
||||||
|
document.addEventListener('touchend', onUp);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Infinite Scroll Helper ────────────────────────────────────
|
||||||
|
window.InfiniteScroll = function(opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var container = opts.container || window;
|
||||||
|
var threshold = opts.threshold || 200;
|
||||||
|
var loading = false;
|
||||||
|
var observer = new IntersectionObserver(function(entries) {
|
||||||
|
if (entries[0].isIntersecting && !loading && opts.onLoad) {
|
||||||
|
loading = true;
|
||||||
|
opts.onLoad(function() { loading = false; });
|
||||||
|
}
|
||||||
|
}, { root: container === window ? null : container, rootMargin: threshold + 'px' });
|
||||||
|
var sentinel = document.createElement('div');
|
||||||
|
sentinel.style.cssText = 'height:1px;';
|
||||||
|
(opts.sentinelParent || document.body).appendChild(sentinel);
|
||||||
|
observer.observe(sentinel);
|
||||||
|
return { disconnect: function() { observer.disconnect(); sentinel.remove(); } };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Sparkline Renderer ────────────────────────────────────────
|
||||||
|
window.renderSparkline = function(containerSelector, values, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var container = typeof containerSelector === 'string' ? document.querySelector(containerSelector) : containerSelector;
|
||||||
|
if (!container || !values || !values.length) return;
|
||||||
|
var max = Math.max.apply(null, values) || 1;
|
||||||
|
var min = Math.min.apply(null, values);
|
||||||
|
var range = max - min || 1;
|
||||||
|
var cls = opts.trend === 'up' ? 'sparkline--up' : (opts.trend === 'down' ? 'sparkline--down' : '');
|
||||||
|
var html = '<div class="sparkline ' + cls + '">';
|
||||||
|
values.forEach(function(v) {
|
||||||
|
var h = Math.max(4, Math.round(((v - min) / range) * 100));
|
||||||
|
html += '<div class="sparkline__bar" style="height:' + h + '%" title="' + (opts.prefix || '') + v + '"></div>';
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -34,6 +34,16 @@
|
|||||||
<span class="theme-bar__label">Sucursal Centro — Usuario: H. García</span>
|
<span class="theme-bar__label">Sucursal Centro — Usuario: H. García</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-bar__right">
|
<div class="theme-bar__right">
|
||||||
|
<button class="icon-btn" id="notifDropdownBtn" title="Notificaciones" onclick="NotificationsDropdown.toggle()" style="margin-right:8px;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||||
|
<span class="notif-dot" id="headerNotifDot"></span>
|
||||||
|
</button>
|
||||||
|
<button class="icon-btn" title="Modo táctil" onclick="TouchModeToggle.set(document.documentElement.getAttribute('data-touch') !== 'true')" style="margin-right:4px;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="icon-btn" title="Densidad" onclick="DensityToggle.set(document.documentElement.getAttribute('data-density') === 'compact' ? 'normal' : 'compact')" style="margin-right:8px;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
<span class="theme-bar__label">Tema:</span>
|
<span class="theme-bar__label">Tema:</span>
|
||||||
<button class="theme-btn theme-btn--industrial is-active" data-theme-target="industrial" onclick="setTheme('industrial')">
|
<button class="theme-btn theme-btn--industrial is-active" data-theme-target="industrial" onclick="setTheme('industrial')">
|
||||||
<span class="theme-btn__swatch"></span>
|
<span class="theme-btn__swatch"></span>
|
||||||
@@ -908,6 +918,60 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ Product Timeline Modal ══════════ -->
|
||||||
|
<div class="inv-modal-overlay" id="productTimelineModal">
|
||||||
|
<div class="inv-modal inv-modal--wide">
|
||||||
|
<div class="inv-modal__header">
|
||||||
|
<h3>Timeline del Producto</h3>
|
||||||
|
<button class="inv-modal__close" onclick="document.getElementById('productTimelineModal').classList.remove('is-open')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="inv-modal__body" id="productTimelineBody">
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="timeline__item"><div class="timeline__dot"></div><div class="timeline__content"><div class="timeline__date">--</div><div class="timeline__title">Producto creado</div></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inv-modal__footer">
|
||||||
|
<button class="btn btn--ghost" onclick="document.getElementById('productTimelineModal').classList.remove('is-open')">Cerrar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ Image Comparator Modal ══════════ -->
|
||||||
|
<div class="inv-modal-overlay" id="imageCompareModal">
|
||||||
|
<div class="inv-modal">
|
||||||
|
<div class="inv-modal__header">
|
||||||
|
<h3>Comparar Imágenes</h3>
|
||||||
|
<button class="inv-modal__close" onclick="document.getElementById('imageCompareModal').classList.remove('is-open')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="inv-modal__body">
|
||||||
|
<div class="img-compare" id="imgCompareContainer" style="max-height:400px;">
|
||||||
|
<img class="img-compare__img" src="" id="imgCompareNew" alt="Nueva" />
|
||||||
|
<div class="img-compare__overlay">
|
||||||
|
<img src="" id="imgCompareOld" alt="Anterior" />
|
||||||
|
</div>
|
||||||
|
<div class="img-compare__handle">↔</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════ Ticket Preview Modal ══════════ -->
|
||||||
|
<div class="inv-modal-overlay" id="ticketPreviewModal">
|
||||||
|
<div class="inv-modal" style="max-width:400px;">
|
||||||
|
<div class="inv-modal__header">
|
||||||
|
<h3>Vista previa del ticket</h3>
|
||||||
|
<button class="inv-modal__close" onclick="document.getElementById('ticketPreviewModal').classList.remove('is-open')">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="inv-modal__body" id="ticketPreviewBody">
|
||||||
|
<!-- Populated by JS -->
|
||||||
|
</div>
|
||||||
|
<div class="inv-modal__footer">
|
||||||
|
<button class="btn btn--ghost" onclick="document.getElementById('ticketPreviewModal').classList.remove('is-open')">Cerrar</button>
|
||||||
|
<button class="btn btn--primary" onclick="window.print()">Imprimir</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Offline Banner -->
|
<!-- Offline Banner -->
|
||||||
<div id="offlineBanner" class="banner banner--warning" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;border-radius:0;animation:none;">
|
<div id="offlineBanner" class="banner banner--warning" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;border-radius:0;animation:none;">
|
||||||
<span class="banner__icon"></span>
|
<span class="banner__icon"></span>
|
||||||
|
|||||||
Reference in New Issue
Block a user