feat(ui): POS UI polish kit — skeletons, toasts, empty states, Cmd+K, tooltips, badges, scrollbars, focus rings, bulk toolbar, breadcrumbs, avatars, connection indicator, sparklines, animations, touch mode, image comparator, ticket preview, resizable columns, sticky headers, density mode

This commit is contained in:
2026-05-26 08:39:32 +00:00
parent 3060dab471
commit 23dbf54f3f
18 changed files with 1060 additions and 44 deletions

View File

@@ -49,6 +49,11 @@
// --- Dashboard summary badges ---
function loadSummary() {
var skeletonHtml = '<div class="skeleton skeleton--text" style="width:80%;"></div>';
['inv-total-skus','inv-total-value','inv-low-stock','inv-no-movement'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.innerHTML = skeletonHtml;
});
apiFetch(API + '/summary').then(function(data) {
if (!data) return;
var totalSkusEl = document.getElementById('inv-total-skus');
@@ -112,6 +117,17 @@
});
}
// Register Cmd+K items
if (typeof registerCmdKItem === 'function') {
registerCmdKItem({ group: 'Inventario', label: 'Ver stock', href: '/pos/inventory#stock', icon: '📦' });
registerCmdKItem({ group: 'Inventario', label: 'Alertas de stock', href: '/pos/inventory#alertas', icon: '⚠️' });
registerCmdKItem({ group: 'Inventario', label: 'Entradas de mercancía', href: '/pos/inventory#entradas', icon: '📥' });
registerCmdKItem({ group: 'Inventario', label: 'Salidas / Ventas', href: '/pos/inventory#salidas', icon: '📤' });
registerCmdKItem({ group: 'Inventario', label: 'Traspasos', href: '/pos/inventory#traspasos', icon: '🚚' });
registerCmdKItem({ group: 'Inventario', label: 'Ajustes', href: '/pos/inventory#ajustes', icon: '⚙️' });
registerCmdKItem({ group: 'Inventario', label: 'Conteos físicos', href: '/pos/inventory#conteos', icon: '🔢' });
}
// Handle hash-based tab switching (e.g. /pos/inventory#alertas)
(function handleHashTab() {
var hash = window.location.hash.replace('#', '');
@@ -206,13 +222,20 @@
var params = new URLSearchParams({ page: currentPage, per_page: 50 });
if (currentSearch) params.set('q', currentSearch);
var tbody = document.getElementById('productTableBody');
if (tbody) tbody.innerHTML = renderSkeletonRows(12, 8);
apiFetch(API + '/items?' + params.toString()).then(function (data) {
if (!data) return;
var tbody = document.getElementById('productTableBody');
var items = data.data || [];
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="12" style="text-align:center;padding:30px;color:var(--color-text-muted);">Sin productos</td></tr>';
tbody.innerHTML = '<tr><td colspan="12">' + renderEmptyState({
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>',
title: 'Sin productos',
subtitle: currentSearch ? 'No se encontraron resultados para "' + esc(currentSearch) + '". Intenta con otro término.' : 'El inventario está vacío. Crea tu primer producto para empezar.',
action: currentSearch ? '<button class="btn btn--ghost btn--sm" onclick="document.getElementById(\'productSearch\').value=\'\';loadItems(1,\'\')">Limpiar búsqueda</button>' : '<button class="btn btn--primary btn--sm" onclick="openCreateModal()">Crear producto</button>'
}) + '</td></tr>';
document.getElementById('productPagination').innerHTML = '';
return;
}
@@ -223,7 +246,7 @@
rowHeight: 48,
buffer: 3,
renderRow: renderInventoryRow,
emptyHtml: '<tr><td colspan="12" style="text-align:center;padding:30px;color:var(--color-text-muted);">Sin productos</td></tr>'
emptyHtml: '<tr><td colspan="12">' + renderEmptyState({ title: 'Sin productos', subtitle: 'El inventario está vacío.' }) + '</td></tr>'
});
}
inventoryVS.setData(items);
@@ -653,14 +676,24 @@
// =====================================================================
function loadAlerts() {
var container = document.getElementById('alertsContent');
if (container) container.innerHTML = '<div style="padding:var(--space-6);">' + renderEmptyState({
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
title: 'Cargando alertas...',
subtitle: 'Revisando el estado del inventario'
}) + '</div>';
apiFetch(API + '/alerts').then(function (data) {
if (!data) return;
var alerts = data.data || [];
var container = document.getElementById('alertsContent');
if (!container) return;
if (!alerts.length) {
container.innerHTML = '<p style="padding:var(--space-6);text-align:center;color:var(--color-text-muted);">Sin alertas activas</p>';
container.innerHTML = renderEmptyState({
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
title: 'Todo en orden',
subtitle: 'No hay alertas activas en el inventario. Los niveles de stock están dentro de los límites configurados.'
});
return;
}
@@ -726,7 +759,11 @@
var history = data.data || [];
var html = '';
if (!history.length) {
html = '<p style="color:var(--color-text-muted);text-align:center;padding:var(--space-4);">Sin movimientos</p>';
html = renderEmptyState({
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="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
title: 'Sin movimientos',
subtitle: 'Este producto aún no tiene historial de entradas, salidas ni ajustes.'
});
} else {
html = '<table class="data-table"><thead><tr><th>Fecha</th><th>Tipo</th><th>Cantidad</th><th>Costo</th><th>Empleado</th><th>Notas</th></tr></thead><tbody>';
history.forEach(function (h) {
@@ -763,11 +800,11 @@
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
}).then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) { alert('Error: ' + data.error); return; }
showToast('Artículo eliminado');
if (data.error) { showToast(data.error, 'error', { title: 'Error al eliminar' }); return; }
showToast('El artículo fue eliminado correctamente.', 'ok', { title: 'Eliminado' });
loadItems(currentPage);
if (window.loadInventoryStats) window.loadInventoryStats();
}).catch(function() { alert('Error al eliminar artículo'); });
}).catch(function() { showToast('No se pudo eliminar el artículo. Intenta de nuevo.', 'error', { title: 'Error' }); });
}
// =====================================================================