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:
@@ -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' }); });
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
|
||||
Reference in New Issue
Block a user