/* ========================================================================== NEXUS POS — Onboarding Wizard for New Tenants Shows a step-by-step setup guide on first login. Persists completion in localStorage('pos_onboarding_done') and server. ========================================================================== */ (function () { 'use strict'; /* ------------------------------------------------------------------ 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 ------------------------------------------------------------------ */ var currentStep = 0; var totalSteps = 5; var overlay, body; var stepState = { branchRenamed: false, productCreated: false, employeeCreated: false }; /* ------------------------------------------------------------------ HELPERS ------------------------------------------------------------------ */ function h(tag, attrs, children) { var el = document.createElement(tag); if (attrs) Object.keys(attrs).forEach(function (k) { if (k === 'className') el.className = attrs[k]; else if (k.indexOf('on') === 0) el.addEventListener(k.slice(2).toLowerCase(), attrs[k]); else el.setAttribute(k, attrs[k]); }); if (children) { if (!Array.isArray(children)) children = [children]; children.forEach(function (c) { if (typeof c === 'string') el.appendChild(document.createTextNode(c)); else if (c) el.appendChild(c); }); } return el; } function getToken() { try { return localStorage.getItem('pos_token') || ''; } catch (e) { return ''; } } async function api(url, opts) { var token = getToken(); var headers = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = 'Bearer ' + token; var res = await fetch(url, Object.assign({ headers: headers }, opts || {})); var json = await res.json(); if (!res.ok) throw new Error(json.error || 'Error ' + res.status); return json; } /* ------------------------------------------------------------------ STEP RENDERERS Each returns a DOM fragment for the body area. ------------------------------------------------------------------ */ function renderStep0() { /* Welcome */ var businessName = ''; try { businessName = localStorage.getItem('pos_business_name') || ''; } catch (e) {} var greeting = businessName ? businessName : 'tu negocio'; return h('div', { className: 'onb-step-enter' }, [ h('div', { className: 'onb-icon' }, ['\uD83D\uDE80']), h('h2', { className: 'onb-title' }, 'Bienvenido a Nexus POS'), h('p', { className: 'onb-desc' }, 'Vamos a configurar ' + greeting + ' en unos minutos. Este asistente te guiara por los pasos esenciales para empezar a vender.'), ]); } function renderStep1() { /* Branch rename */ var container = h('div', { className: 'onb-step-enter' }, [ h('div', { className: 'onb-icon' }, ['\uD83C\uDFEA']), h('h2', { className: 'onb-title' }, 'Tu Primera Sucursal'), h('p', { className: 'onb-desc' }, 'Ya creamos la sucursal "Principal" para ti. Puedes renombrarla si quieres.'), ]); var input = h('input', { className: 'onb-input', type: 'text', placeholder: 'Principal', value: 'Principal', id: 'onb-branch-name' }); var msg = h('div', { className: 'onb-error', id: 'onb-branch-msg' }); container.appendChild(h('div', { className: 'onb-form' }, [ h('div', { className: 'onb-field' }, [ h('label', { className: 'onb-label' }, 'Nombre de sucursal'), input, msg ]) ])); return container; } function renderStep2() { /* Add first product */ var container = h('div', { className: 'onb-step-enter' }, [ h('div', { className: 'onb-icon' }, ['\uD83D\uDCE6']), h('h2', { className: 'onb-title' }, 'Agrega Tu Primer Producto'), h('p', { className: 'onb-desc' }, 'Registra una pieza para probar el sistema. Despues podras agregar mas desde Inventario.'), ]); if (stepState.productCreated) { container.appendChild(h('div', { className: 'onb-success' }, [ '\u2705 ', 'Producto creado exitosamente.' ])); return container; } var error = h('div', { className: 'onb-error', id: 'onb-product-msg' }); container.appendChild(h('div', { className: 'onb-form' }, [ h('div', { className: 'onb-field' }, [ h('label', { className: 'onb-label' }, 'Numero de parte'), h('input', { className: 'onb-input', type: 'text', id: 'onb-pn', placeholder: 'Ej: FIL-ACE-001' }) ]), h('div', { className: 'onb-field' }, [ h('label', { className: 'onb-label' }, 'Nombre'), h('input', { className: 'onb-input', type: 'text', id: 'onb-pname', placeholder: 'Ej: Filtro de aceite' }) ]), h('div', { className: 'onb-field' }, [ h('label', { className: 'onb-label' }, 'Precio de venta ($)'), h('input', { className: 'onb-input', type: 'number', id: 'onb-pprice', placeholder: '0.00', min: '0', step: '0.01' }) ]), h('div', { className: 'onb-field' }, [ h('label', { className: 'onb-label' }, 'Stock inicial'), h('input', { className: 'onb-input', type: 'number', id: 'onb-pstock', placeholder: '0', min: '0', step: '1' }) ]), error ])); return container; } function renderStep3() { /* Create employee */ var container = h('div', { className: 'onb-step-enter' }, [ h('div', { className: 'onb-icon' }, ['\uD83D\uDC64']), h('h2', { className: 'onb-title' }, 'Crea Tu Primer Empleado'), h('p', { className: 'onb-desc' }, 'Agrega un usuario para el punto de venta. El PIN se usara para iniciar turno.'), ]); if (stepState.employeeCreated) { container.appendChild(h('div', { className: 'onb-success' }, [ '\u2705 ', 'Empleado creado exitosamente.' ])); return container; } var error = h('div', { className: 'onb-error', id: 'onb-emp-msg' }); container.appendChild(h('div', { className: 'onb-form' }, [ h('div', { className: 'onb-field' }, [ h('label', { className: 'onb-label' }, 'Nombre'), h('input', { className: 'onb-input', type: 'text', id: 'onb-ename', placeholder: 'Ej: Juan Perez' }) ]), h('div', { className: 'onb-field' }, [ h('label', { className: 'onb-label' }, 'PIN (4 digitos)'), h('input', { className: 'onb-input', type: 'password', id: 'onb-epin', placeholder: '****', maxlength: '4' }) ]), h('div', { className: 'onb-field' }, [ h('label', { className: 'onb-label' }, 'Rol'), h('select', { className: 'onb-select', id: 'onb-erole' }, [ h('option', { value: 'cashier' }, 'Cajero'), h('option', { value: 'warehouse' }, 'Almacenista'), h('option', { value: 'admin' }, 'Administrador'), h('option', { value: 'accountant' }, 'Contador') ]) ]), error ])); return container; } function renderStep4() { /* Done */ return h('div', { className: 'onb-step-enter' }, [ h('div', { className: 'onb-icon' }, ['\u2705']), h('h2', { className: 'onb-title' }, 'Listo! Tu Sistema Esta Configurado'), h('p', { className: 'onb-desc' }, 'Ya puedes empezar a usar Nexus POS. Aqui tienes accesos rapidos:'), h('div', { className: 'onb-links' }, [ h('a', { className: 'onb-link-card', href: '/pos/catalog' }, [ h('span', { className: 'onb-link-icon' }, '\uD83D\uDCD6'), 'Catalogo' ]), h('a', { className: 'onb-link-card', href: '/pos/' }, [ h('span', { className: 'onb-link-icon' }, '\uD83D\uDCBB'), 'Punto de Venta' ]), h('a', { className: 'onb-link-card', href: '/pos/inventory' }, [ h('span', { className: 'onb-link-icon' }, '\uD83D\uDCE6'), 'Inventario' ]), ]) ]); } var stepRenderers = [renderStep0, renderStep1, renderStep2, renderStep3, renderStep4]; /* ------------------------------------------------------------------ ACTION HANDLERS — called when user clicks primary button ------------------------------------------------------------------ */ async function actionStep1() { /* Rename branch (optional — just show success) */ var name = document.getElementById('onb-branch-name'); if (name && name.value.trim()) { stepState.branchRenamed = true; } goNext(); } async function actionStep2() { /* Create product via API */ if (stepState.productCreated) { goNext(); return; } var pn = (document.getElementById('onb-pn') || {}).value || ''; var pname = (document.getElementById('onb-pname') || {}).value || ''; var price = parseFloat((document.getElementById('onb-pprice') || {}).value) || 0; var stock = parseInt((document.getElementById('onb-pstock') || {}).value) || 0; var msg = document.getElementById('onb-product-msg'); if (!pn.trim() || !pname.trim()) { if (msg) msg.textContent = 'Numero de parte y nombre son obligatorios.'; return; } try { if (msg) msg.textContent = ''; await api('/pos/api/inventory/items', { method: 'POST', body: JSON.stringify({ part_number: pn.trim(), name: pname.trim(), price_1: price, initial_stock: stock }) }); stepState.productCreated = true; renderCurrentStep(); setTimeout(goNext, 800); } catch (e) { if (msg) msg.textContent = e.message || 'Error al crear producto.'; } } async function actionStep3() { /* Create employee via API */ if (stepState.employeeCreated) { goNext(); return; } var ename = (document.getElementById('onb-ename') || {}).value || ''; var epin = (document.getElementById('onb-epin') || {}).value || ''; var erole = (document.getElementById('onb-erole') || {}).value || 'cashier'; var msg = document.getElementById('onb-emp-msg'); if (!ename.trim()) { if (msg) msg.textContent = 'El nombre es obligatorio.'; return; } if (!epin || epin.length < 4) { if (msg) msg.textContent = 'El PIN debe tener al menos 4 digitos.'; return; } try { if (msg) msg.textContent = ''; await api('/pos/api/config/employees', { method: 'POST', body: JSON.stringify({ name: ename.trim(), pin: epin, role: erole }) }); stepState.employeeCreated = true; renderCurrentStep(); setTimeout(goNext, 800); } catch (e) { if (msg) msg.textContent = e.message || 'Error al crear empleado.'; } } function actionStep4() { /* Finish */ finish(); } var stepActions = [goNext, actionStep1, actionStep2, actionStep3, actionStep4]; /* ------------------------------------------------------------------ NAVIGATION ------------------------------------------------------------------ */ function goNext() { if (currentStep < totalSteps - 1) { currentStep++; renderCurrentStep(); } } function goBack() { if (currentStep > 0) { currentStep--; renderCurrentStep(); } } function skip() { goNext(); } 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)'; setTimeout(function () { overlay.parentNode.removeChild(overlay); }, 250); } } function dismiss() { finish(); } /* ------------------------------------------------------------------ RENDER ------------------------------------------------------------------ */ function renderCurrentStep() { body.innerHTML = ''; body.appendChild(stepRenderers[currentStep]()); updateFooter(); } function updateFooter() { var footer = overlay.querySelector('.onboarding-footer'); if (!footer) return; footer.innerHTML = ''; /* Action row */ var actions = h('div', { className: 'onb-actions' }); /* Back / skip */ if (currentStep === 0) { actions.appendChild(h('span')); /* spacer */ } else if (currentStep < totalSteps - 1) { actions.appendChild( h('button', { className: 'onb-btn onb-btn--ghost', onClick: skip }, 'Saltar') ); } else { actions.appendChild(h('span')); } /* Primary button */ var labels = ['Empezar', 'Siguiente', 'Guardar Producto', 'Guardar Empleado', 'Ir al Sistema']; var primaryBtn = h('button', { className: 'onb-btn onb-btn--primary', onClick: stepActions[currentStep] }, labels[currentStep]); /* If product/employee already created, change label */ if (currentStep === 2 && stepState.productCreated) primaryBtn.textContent = 'Siguiente'; if (currentStep === 3 && stepState.employeeCreated) primaryBtn.textContent = 'Siguiente'; actions.appendChild(primaryBtn); footer.appendChild(actions); /* Progress dots */ var progressRow = h('div', { className: 'onb-progress' }); for (var i = 0; i < totalSteps; i++) { var dotClass = 'onb-dot'; if (i === currentStep) dotClass += ' is-active'; else if (i < currentStep) dotClass += ' is-done'; progressRow.appendChild(h('div', { className: dotClass })); } footer.appendChild(progressRow); /* Step label */ footer.appendChild( h('div', { className: 'onb-step-label' }, (currentStep + 1) + ' de ' + totalSteps) ); /* Dismiss checkbox on last step */ if (currentStep === totalSteps - 1) { var cb = h('input', { type: 'checkbox', id: 'onb-dismiss-cb', checked: 'checked' }); footer.appendChild(h('div', { className: 'onb-dismiss-row' }, [ cb, h('label', { for: 'onb-dismiss-cb' }, 'No mostrar de nuevo') ])); } } /* ------------------------------------------------------------------ INIT — Build the modal and inject it ------------------------------------------------------------------ */ function init() { overlay = h('div', { className: 'onboarding-overlay' }); /* Click backdrop to dismiss */ overlay.addEventListener('click', function (e) { if (e.target === overlay) dismiss(); }); var modal = h('div', { className: 'onboarding-modal' }); body = h('div', { className: 'onboarding-body' }); var footer = h('div', { className: 'onboarding-footer' }); modal.appendChild(body); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); renderCurrentStep(); } function initWizard() { init(); } /* Wait for DOM, then check server before showing wizard */ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', checkServerAndMaybeInit); } else { checkServerAndMaybeInit(); } })();