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 = '