From 68d6f8167174772b92b0eb2f22f8b1cd69265c98 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 26 May 2026 09:28:35 +0000 Subject: [PATCH] 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 --- pos/static/css/pos-ui.css | 64 +++++++++ pos/static/js/inventory.js | 4 + pos/static/js/pos-utils.js | 271 +++++++++++++++++++++++++++++++++++ pos/templates/inventory.html | 64 +++++++++ 4 files changed, 403 insertions(+) diff --git a/pos/static/css/pos-ui.css b/pos/static/css/pos-ui.css index 86ea43b..32f6652 100644 --- a/pos/static/css/pos-ui.css +++ b/pos/static/css/pos-ui.css @@ -5,6 +5,40 @@ * 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) ═══════════════════════════════════════════════════════════════ */ @@ -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(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 ═══════════════════════════════════════════════════════════════ */ diff --git a/pos/static/js/inventory.js b/pos/static/js/inventory.js index 8f8d1a8..847e569 100644 --- a/pos/static/js/inventory.js +++ b/pos/static/js/inventory.js @@ -250,6 +250,10 @@ }); } inventoryVS.setData(items); + // Make columns resizable + if (typeof makeTableResizable === 'function') { + makeTableResizable('#stockTable'); + } // Pagination var pg = data.pagination || {}; diff --git a/pos/static/js/pos-utils.js b/pos/static/js/pos-utils.js index 928edb2..c13aeaf 100644 --- a/pos/static/js/pos-utils.js +++ b/pos/static/js/pos-utils.js @@ -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 += '' + esc(f.name) + + ''; + }); + 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 = '
Notificaciones
' + + '
Sin notificaciones nuevas
'; + 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 = '
' + + '
⚠️
' + + '
' + count + ' producto' + (count > 1 ? 's' : '') + ' sin stock
' + + '
Stock crítico
'; + } + }).catch(function() {}); + } + }; + + // ── Ticket Preview Helper ───────────────────────────────────── + window.previewTicket = function(ticketData) { + var data = ticketData || {}; + var items = (data.items || []).map(function(it) { + return '
' + (it.quantity || 1) + 'x ' + esc(it.name) + '$' + (it.subtotal || 0).toFixed(2) + '
'; + }).join(''); + var html = '
' + + '
Nexus Autoparts
' + (data.store || 'Sucursal Centro') + '
' + + '
' + new Date().toLocaleString('es-MX') + '
' + + '
Ticket #' + (data.id || '---') + '
' + + '
' + + items + + '
' + + '
Subtotal$' + (data.subtotal || 0).toFixed(2) + '
' + + '
IVA$' + (data.tax || 0).toFixed(2) + '
' + + '
TOTAL$' + (data.total || 0).toFixed(2) + '
' + + '
Gracias por su compra
' + + '
'; + 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 = '
'; + values.forEach(function(v) { + var h = Math.max(4, Math.round(((v - min) / range) * 100)); + html += '
'; + }); + html += '
'; + container.innerHTML = html; + }; + })(); diff --git a/pos/templates/inventory.html b/pos/templates/inventory.html index 2ede72a..ffaf1bf 100644 --- a/pos/templates/inventory.html +++ b/pos/templates/inventory.html @@ -34,6 +34,16 @@ Sucursal Centro — Usuario: H. García
+ + + Tema:
+ +
+
+
+

Timeline del Producto

+ +
+
+
+
--
Producto creado
+
+
+ +
+
+ + +
+
+
+

Comparar Imágenes

+ +
+
+
+ Nueva +
+ Anterior +
+
+
+
+
+
+ + +
+
+
+

Vista previa del ticket

+ +
+
+ +
+ +
+
+