feat(pos): integrate design system for clients, inventory, catalog + offline banner

- Replace customers.html with design system clientes page, add slide panel + wire to customers.js
- Replace inventory.html with design system inventario page, load inventory.js
- Add empty state component to catalog product grid (hidden, shown when no results)
- Add offline banner HTML/CSS to all three pages
- Create offline-banner.js: listens online/offline events, auto-dismisses restored banner after 3s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 07:28:41 +00:00
parent 21427c4dd2
commit 9641b0af80
4 changed files with 5000 additions and 344 deletions

View File

@@ -0,0 +1,46 @@
// /home/Autopartes/pos/static/js/offline-banner.js
// Global offline/online banner controller.
// Include this script in any page that has an #offlineBanner element.
(function () {
'use strict';
var banner = document.getElementById('offlineBanner');
var bannerText = document.getElementById('offlineBannerText');
if (!banner || !bannerText) return;
var dismissTimer = null;
function showOffline() {
clearTimeout(dismissTimer);
banner.className = 'banner banner--error';
banner.style.display = 'flex';
banner.style.animation = 'slideDown 0.35s ease-out forwards';
bannerText.innerHTML = '<strong>Conexion perdida</strong> — Intentando reconectar...';
}
function showOnline() {
clearTimeout(dismissTimer);
banner.className = 'banner banner--success';
banner.style.display = 'flex';
banner.style.animation = 'slideDown 0.35s ease-out forwards';
bannerText.innerHTML = '<strong>Conexion restaurada</strong> — Sincronizando datos...';
// Auto-dismiss after 3 seconds
dismissTimer = setTimeout(function () {
banner.style.animation = 'slideUp 0.3s ease-in forwards';
banner.addEventListener('animationend', function onEnd() {
banner.style.display = 'none';
banner.removeEventListener('animationend', onEnd);
}, { once: true });
}, 3000);
}
// Show warning immediately if already offline
if (!navigator.onLine) {
showOffline();
}
window.addEventListener('offline', showOffline);
window.addEventListener('online', showOnline);
})();

View File

@@ -1388,6 +1388,101 @@
.sidebar-overlay.is-open {
display: block;
}
/* =========================================================================
EMPTY STATE COMPONENT
========================================================================= */
.empty-state {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
max-width: 320px;
padding: var(--space-8) var(--space-4);
margin: var(--space-10) auto;
width: 100%;
}
.empty-state.is-visible {
display: flex;
}
.empty-state__icon {
font-size: 48px;
line-height: 1;
margin-bottom: var(--space-4);
opacity: 0.6;
filter: grayscale(30%);
}
.empty-state__title {
font-family: var(--font-heading);
font-size: var(--text-h5);
font-weight: var(--heading-weight-secondary);
color: var(--color-text-primary);
margin-bottom: var(--space-2);
}
.empty-state__subtitle {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
line-height: 1.5;
margin-bottom: var(--space-5);
}
.empty-state__action {
padding: var(--space-2) var(--space-5);
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: 500;
border-radius: var(--radius-md);
cursor: pointer;
transition: var(--transition-fast);
border: 1px solid var(--btn-secondary-border);
background: var(--btn-secondary-bg);
color: var(--btn-secondary-text);
}
.empty-state__action:hover {
background: var(--btn-secondary-bg-hover);
box-shadow: var(--shadow-sm);
}
/* =========================================================================
OFFLINE BANNER
========================================================================= */
@keyframes slideDown { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes slideUp { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-100%); opacity: 0; } }
.banner {
display: flex; align-items: center; gap: var(--space-3);
padding: var(--space-3) var(--space-4); border-radius: var(--radius-md);
font-size: var(--text-body-sm); font-weight: 500; line-height: 1.4;
}
.banner--warning {
background: var(--color-warning-light, #fef9c3); color: var(--color-warning-dark, #854d0e);
border: 1px solid var(--color-warning, #eab308);
}
.banner--success {
background: var(--color-success-light, #dcfce7); color: var(--color-success-dark, #166534);
border: 1px solid var(--color-success, #22c55e);
}
.banner--error {
background: var(--color-error-light, #fef2f2); color: var(--color-error-dark, #991b1b);
border: 1px solid var(--color-error, #ef4444);
}
.banner--dismissing { animation: slideUp 0.3s ease-in forwards; }
.banner__icon { font-size: 18px; flex-shrink: 0; }
.banner__text { flex: 1; }
.banner__text strong { font-weight: 700; }
.banner__dismiss {
background: none; border: none; cursor: pointer; font-size: 18px;
padding: var(--space-1); opacity: 0.7; color: inherit;
}
.banner__dismiss:hover { opacity: 1; }
</style>
</head>
@@ -1871,6 +1966,14 @@
</div>
<!-- /product-grid -->
<!-- Empty State (shown when no results) -->
<div class="empty-state" id="emptyState">
<div class="empty-state__icon">&#128270;</div>
<div class="empty-state__title" id="emptyStateTitle">No se encontraron productos</div>
<div class="empty-state__subtitle" id="emptyStateSubtitle">Intenta con otro termino de busqueda o verifica el numero de parte</div>
<button class="empty-state__action" onclick="document.querySelector('.search-box input').value=''; document.querySelector('.search-box input').focus();">Limpiar filtros</button>
</div>
<!-- Pagination -->
<nav class="pagination" aria-label="Paginación de resultados">
<button class="page-item page-item--wide is-disabled" aria-label="Página anterior" disabled>
@@ -2069,5 +2172,13 @@
});
</script>
<!-- Offline Banner -->
<div id="offlineBanner" class="banner banner--warning" style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;border-radius:0;animation:none;">
<span class="banner__icon"></span>
<span class="banner__text" id="offlineBannerText"><strong>Modo offline</strong> — Funciones limitadas. Solo consultas en cache disponibles.</span>
<button class="banner__dismiss" onclick="document.getElementById('offlineBanner').style.display='none'" aria-label="Cerrar">&times;</button>
</div>
<script src="/pos/static/js/offline-banner.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff