feat(ui): infinite scroll, saved filters, product timeline, image comparator, customers bulk toolbar, dark mode refinements

This commit is contained in:
2026-05-26 09:37:35 +00:00
parent 5c815bc2f5
commit c5fc8c5ec6
5 changed files with 159 additions and 1 deletions

View File

@@ -819,3 +819,43 @@ input:disabled, select:disabled, textarea:disabled {
background: var(--color-surface-1, #1a1a1a);
border-bottom: 2px solid var(--color-primary, #F5A623);
}
/* Enhanced depth layers for both themes */
[data-theme="modern"] {
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
--shadow-md: 0 4px 12px rgba(0,0,0,0.4);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.5);
--shadow-xl: 0 16px 48px rgba(0,0,0,0.6);
}
[data-theme="industrial"] {
--shadow-sm: 0 1px 2px rgba(0,0,0,0.4);
--shadow-md: 0 4px 12px rgba(0,0,0,0.5);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.6);
--shadow-xl: 0 16px 48px rgba(0,0,0,0.7);
}
/* Smooth theme transition (only on properties that don't cause flash) */
.card, .glass-card, .btn, .icon-btn, .kpi-card, .alert-card, .meli-card {
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
/* Improved text rendering in dark */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Focus visible for accessibility */
*:focus-visible {
outline: 2px solid var(--color-primary, #F5A623);
outline-offset: 2px;
}
/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@@ -94,6 +94,8 @@ const Customers = (() => {
}
}
var selectedCustomers = new Set();
function renderCustomerRow(c) {
const tier = tierMap[c.price_tier] || 'Mostrador';
const tClass = tierClass[c.price_tier] || 'mostrador';
@@ -104,7 +106,9 @@ const Customers = (() => {
const creditClass = usedPct >= 80 ? 'none' : usedPct >= 60 ? 'low' : '';
const num = String(c.id).padStart(5, '0');
const selClass = (currentCustomer && currentCustomer.id === c.id) ? 'selected' : '';
return '<tr class="' + selClass + '" onclick="Customers.selectCustomer(' + c.id + ')">' +
const isChecked = selectedCustomers.has(c.id) ? 'checked' : '';
return '<tr class="' + selClass + '">' +
'<td onclick="event.stopPropagation();"><input type="checkbox" ' + isChecked + ' onchange="Customers.toggleCustomerSelection(' + c.id + ')"></td>' +
'<td class="cell-num">' + num + '</td>' +
'<td>' +
'<div class="cell-name">' + (c.name || '') + '</div>' +
@@ -801,6 +805,41 @@ const Customers = (() => {
showPaymentModal, closePayment, recordPayment,
};
// Bulk selection
publicApi.toggleCustomerSelection = function(id) {
if (selectedCustomers.has(id)) selectedCustomers.delete(id);
else selectedCustomers.add(id);
updateBulkToolbar();
};
publicApi.toggleSelectAll = function() {
var cb = document.getElementById('selectAllCustomers');
var allChecked = cb.checked;
if (customersVS && customersVS.data) {
customersVS.data.forEach(function(c) {
if (allChecked) selectedCustomers.add(c.id);
else selectedCustomers.delete(c.id);
});
customersVS.refresh();
}
updateBulkToolbar();
};
function updateBulkToolbar() {
var container = document.getElementById('customersBulkToolbar');
if (!container) return;
var count = selectedCustomers.size;
if (count === 0) { container.innerHTML = ''; return; }
container.innerHTML = renderBulkToolbar(count,
'<button class="btn btn--primary btn--sm" onclick="Customers.featureProximamente(\'Exportar seleccionados\')">📥 Exportar</button>' +
'<button class="btn btn--ghost btn--sm" onclick="Customers.clearSelection()">Limpiar</button>'
);
}
publicApi.clearSelection = function() {
selectedCustomers.clear();
document.getElementById('selectAllCustomers').checked = false;
if (customersVS) customersVS.refresh();
updateBulkToolbar();
};
// Expose globally for inline HTML onclick handlers
window.Customers = publicApi;
return publicApi;

View File

@@ -1314,6 +1314,15 @@
html += '<span id="imgUploadStatus" style="display:block;margin-top:4px;font-size:var(--text-caption);color:var(--color-text-muted);"></span>';
html += '</div>';
// Action buttons
html += '<div style="display:flex;gap:8px;justify-content:center;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
html += '<button class="btn btn--ghost btn--sm" onclick="showProductTimeline(' + data.id + ')">📅 Timeline</button>';
if (data.image_url) {
html += '<button class="btn btn--ghost btn--sm" onclick="showImageCompare(\'' + esc(data.image_url) + '\')">🖼️ Comparar</button>';
}
html += '<button class="btn btn--ghost btn--sm" onclick="viewHistory(' + data.id + ')">📜 Historial</button>';
html += '</div>';
// Product info header
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">ID Inventario</span><strong style="font-family:var(--font-mono);">' + data.id + '</strong></div>';
@@ -1535,9 +1544,76 @@
window.autoMatchCompat = autoMatchCompat;
window.removeCompat = removeCompat;
// ─── Product Timeline ──────────────────────────────────────────
window.showProductTimeline = function(itemId) {
var modal = document.getElementById('productTimelineModal');
var body = document.getElementById('productTimelineBody');
body.innerHTML = '<div style="padding:20px;"><div class="skeleton skeleton--text"></div><div class="skeleton skeleton--text-sm"></div></div>';
modal.classList.add('is-open');
apiFetch(API + '/items/' + itemId + '/history').then(function(data) {
var history = (data && data.data) ? data.data : [];
var html = '<div class="timeline">';
html += '<div class="timeline__item"><div class="timeline__dot timeline__dot--green"></div><div class="timeline__content"><div class="timeline__date">Producto creado</div><div class="timeline__title">Registro inicial en inventario</div></div></div>';
history.forEach(function(h) {
var color = h.quantity > 0 ? 'timeline__dot--green' : (h.quantity < 0 ? 'timeline__dot--red' : 'timeline__dot--blue');
var title = (h.type || 'Movimiento') + ' · ' + (h.quantity > 0 ? '+' : '') + h.quantity + ' unidades';
html += '<div class="timeline__item"><div class="timeline__dot ' + color + '"></div><div class="timeline__content">' +
'<div class="timeline__date">' + esc(h.date) + ' · ' + esc(h.employee) + '</div>' +
'<div class="timeline__title">' + esc(title) + '</div>' +
(h.notes ? '<div class="timeline__desc">' + esc(h.notes) + '</div>' : '') +
'</div></div>';
});
html += '</div>';
body.innerHTML = html;
});
};
// ─── Image Comparator ──────────────────────────────────────────
window.showImageCompare = function(imageUrl) {
var modal = document.getElementById('imageCompareModal');
document.getElementById('imgCompareNew').src = imageUrl + '?t=' + Date.now();
document.getElementById('imgCompareOld').src = imageUrl + '?t=' + (Date.now() - 1);
modal.classList.add('is-open');
setTimeout(function() { if (typeof initImageComparator === 'function') initImageComparator('#imgCompareContainer'); }, 100);
};
// ─── Infinite Scroll ───────────────────────────────────────────
var _infiniteScrollInstance = null;
function setupInfiniteScroll() {
if (_infiniteScrollInstance) _infiniteScrollInstance.disconnect();
var sentinel = document.createElement('div');
sentinel.id = 'inventoryScrollSentinel';
sentinel.style.cssText = 'height:1px;';
var wrapper = document.querySelector('.table-wrapper');
if (wrapper) wrapper.appendChild(sentinel);
_infiniteScrollInstance = new InfiniteScroll({
sentinelParent: wrapper,
onLoad: function(done) {
if (!currentSearch && currentPage < (window._inventoryTotalPages || 999)) {
loadItems(currentPage + 1);
}
if (done) done();
}
});
}
// ─── Saved Filters ─────────────────────────────────────────────
function renderSavedFilters() {
var container = document.getElementById('savedFiltersContainer');
if (!container) return;
SavedFilters.renderChips('savedFiltersContainer', function(filters) {
if (filters.search) {
var el = document.getElementById('productSearch');
if (el) { el.value = filters.search; loadItems(1, filters.search); }
}
});
}
// =====================================================================
// INIT — load stock on page load
// =====================================================================
loadItems(1);
renderSavedFilters();
})();