fix(config): prevent card text overflow; fix(onboarding): persist completion server-side

Cards/Grid:
- Add min-width:0 to .device-grid to prevent grid overflow
- Add max-width:100%, overflow:hidden, word-break:break-word to .device-card
- Add min-width:0 and overflow-wrap to .device-card__body
- Bump config.css cache-bust to v=2

Onboarding:
- Add GET/POST /pos/api/config/onboarding-status endpoints in config_bp.py
- onboarding.js now checks server first before showing wizard
- On finish, POSTs completion to server (tenant_config table)
- Falls back to localStorage for fast path and offline resilience
- Bump onboarding.js cache-bust to v=2 in catalog.html
This commit is contained in:
2026-05-18 07:31:31 +00:00
parent dbf45e374b
commit 0b1dc89faf
4 changed files with 80 additions and 11 deletions

View File

@@ -577,3 +577,34 @@ def update_whatsapp_config():
conn.close()
return jsonify({'message': 'WhatsApp configuration updated'})
@config_bp.route('/onboarding-status', methods=['GET'])
@require_auth('pos.view')
def get_onboarding_status():
"""Check if tenant onboarding wizard has been completed."""
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
cur.execute("SELECT value FROM tenant_config WHERE key = 'onboarding_completed'")
row = cur.fetchone()
cur.close()
conn.close()
return jsonify({'completed': row[0] == 'true' if row else False})
@config_bp.route('/onboarding-status', methods=['POST'])
@require_auth('pos.view')
def set_onboarding_status():
"""Mark tenant onboarding wizard as completed."""
data = request.get_json() or {}
completed = 'true' if data.get('completed') else 'false'
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
cur.execute("""
INSERT INTO tenant_config (key, value) VALUES (%s, %s)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
""", ('onboarding_completed', completed))
conn.commit()
cur.close()
conn.close()
return jsonify({'completed': completed == 'true'})

View File

@@ -1029,6 +1029,7 @@
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--space-4);
min-width: 0;
}
.device-card {
@@ -1042,7 +1043,10 @@
box-shadow: var(--shadow-sm);
transition: var(--transition-normal);
min-width: 0;
max-width: 100%;
overflow: hidden;
overflow-wrap: break-word;
word-break: break-word;
}
[data-theme="modern"] .device-card {

View File

@@ -1,18 +1,41 @@
/* ==========================================================================
NEXUS POS — Onboarding Wizard for New Tenants
Shows a step-by-step setup guide on first login.
Persists completion in localStorage('pos_onboarding_done').
Persists completion in localStorage('pos_onboarding_done') and server.
========================================================================== */
(function () {
'use strict';
/* ------------------------------------------------------------------
GUARD — skip if already completed
GUARD — skip if already completed locally (fast path)
------------------------------------------------------------------ */
if (localStorage.getItem('pos_onboarding_done') === 'true') return;
/* ------------------------------------------------------------------
CHECK SERVER — if completed on server, cache locally and skip
------------------------------------------------------------------ */
function checkServerAndMaybeInit() {
var token = '';
try { token = localStorage.getItem('pos_token') || ''; } catch (e) {}
fetch('/pos/api/config/onboarding-status', {
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
}).then(function (r) {
if (!r.ok) return null;
return r.json();
}).then(function (data) {
if (data && data.completed) {
localStorage.setItem('pos_onboarding_done', 'true');
return;
}
initWizard();
}).catch(function () {
initWizard();
});
}
/* ------------------------------------------------------------------
STATE
------------------------------------------------------------------ */
@@ -335,6 +358,13 @@
function finish() {
localStorage.setItem('pos_onboarding_done', 'true');
var token = '';
try { token = localStorage.getItem('pos_token') || ''; } catch (e) {}
fetch('/pos/api/config/onboarding-status', {
method: 'POST',
headers: token ? { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: true })
}).catch(function () {});
if (overlay && overlay.parentNode) {
overlay.style.opacity = '0';
overlay.style.transition = 'opacity var(--duration-normal) var(--ease-in)';
@@ -439,11 +469,15 @@
renderCurrentStep();
}
/* Wait for DOM */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
function initWizard() {
init();
}
/* Wait for DOM, then check server before showing wizard */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkServerAndMaybeInit);
} else {
checkServerAndMaybeInit();
}
})();

View File

@@ -276,10 +276,10 @@
<h2 style="margin:0;font-family:var(--font-heading);font-size:var(--text-h3);">Catalogo por Marca</h2>
<button onclick="BrandCatalog.hide()" class="btn btn--sm" style="background:none;border:1px solid var(--color-border);color:var(--color-text-primary);padding:8px 16px;border-radius:var(--radius-md);cursor:pointer;">&#10005; Cerrar</button>
</div>
<div id="brandCatalogBreadcrumb" style="margin-bottom:var(--space-3);color:var(--color-text-muted);font-size:var(--text-body-sm);"></div>
<div id="brandCatalogBreadcrumb" style="margin-bottom:var(--space-3);"></div>
<div id="brandCatalogSearch" style="margin-bottom:var(--space-3);"></div>
<div id="brandCatalogLoading" style="display:none;text-align:center;padding:var(--space-8);"><div class="spinner"></div></div>
<div id="brandCatalogContent" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--space-3);"></div>
<div id="brandCatalogLoading" class="loading"><div class="spinner"></div></div>
<div id="brandCatalogContent"></div>
</div>
</div>
@@ -292,7 +292,7 @@
<script src="/pos/static/js/offline-banner.js" defer></script>
<script src="/pos/static/js/chat.js" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script>
<script src="/pos/static/js/onboarding.js" defer></script>
<script src="/pos/static/js/onboarding.js?v=2" defer></script>
<script>
if('serviceWorker' in navigator){
navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'}).then(function(reg){
@@ -308,6 +308,6 @@
}
</script>
<script src="/pos/static/js/pwa-install.js" defer></script>
<script src="/pos/static/js/brand-catalog.js?v=4" defer></script>
<script src="/pos/static/js/brand-catalog.js?v=9" defer></script>
</body>
</html>