diff --git a/pos/static/css/onboarding.css b/pos/static/css/onboarding.css new file mode 100644 index 0000000..94165ad --- /dev/null +++ b/pos/static/css/onboarding.css @@ -0,0 +1,428 @@ +/* ========================================================================== + NEXUS POS — Onboarding Wizard + Uses design system tokens (works with both industrial + modern themes) + ========================================================================== */ + +/* Overlay backdrop */ +.onboarding-overlay { + position: fixed; + inset: 0; + z-index: var(--z-modal); + display: flex; + align-items: center; + justify-content: center; + background: var(--overlay-backdrop); + opacity: 0; + animation: onb-fade-in var(--duration-normal) var(--ease-out) forwards; +} + +@keyframes onb-fade-in { + to { opacity: 1; } +} + +/* Modal card */ +.onboarding-modal { + width: 92vw; + max-width: 500px; + max-height: 90vh; + background: var(--color-bg-elevated); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-xl); + display: flex; + flex-direction: column; + overflow: hidden; + transform: translateY(20px); + animation: onb-slide-up var(--duration-normal) var(--ease-out) forwards; +} + +[data-theme="industrial"] .onboarding-modal { + border-radius: 0; + clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 0 100%); +} + +[data-theme="modern"] .onboarding-modal { + border-radius: var(--radius-lg); +} + +@keyframes onb-slide-up { + to { transform: translateY(0); } +} + +/* Step content area */ +.onboarding-body { + padding: var(--space-8) var(--space-6); + overflow-y: auto; + flex: 1; +} + +/* Step icon */ +.onb-icon { + width: 64px; + height: 64px; + margin: 0 auto var(--space-5); + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + background: var(--color-primary-muted); + color: var(--color-primary); +} + +[data-theme="industrial"] .onb-icon { + border-radius: 0; + clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%); +} + +[data-theme="modern"] .onb-icon { + border-radius: var(--radius-lg); +} + +/* Titles */ +.onb-title { + font-family: var(--font-heading); + font-weight: var(--heading-weight-primary); + font-size: var(--text-h4); + color: var(--color-text-primary); + text-align: center; + margin-bottom: var(--space-2); + letter-spacing: var(--tracking-snug); +} + +[data-theme="industrial"] .onb-title { + text-transform: uppercase; +} + +.onb-desc { + font-size: var(--text-body-sm); + color: var(--color-text-secondary); + text-align: center; + line-height: var(--leading-body-sm); + margin-bottom: var(--space-6); +} + +/* Form fields inside wizard */ +.onb-form { + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.onb-field { + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +.onb-label { + font-size: var(--text-label); + font-weight: var(--font-weight-semibold); + color: var(--color-text-secondary); + letter-spacing: var(--tracking-wide); +} + +[data-theme="industrial"] .onb-label { + text-transform: uppercase; + font-size: var(--text-caption); + letter-spacing: var(--tracking-wider); +} + +.onb-input { + height: 40px; + padding: 0 var(--space-3); + background: var(--color-bg-base); + border: 1px solid var(--color-border); + color: var(--color-text-primary); + font-family: var(--font-body); + font-size: var(--text-body-sm); + transition: var(--transition-fast); + outline: none; +} + +[data-theme="industrial"] .onb-input { + border-radius: 0; +} + +[data-theme="modern"] .onb-input { + border-radius: var(--radius-md); +} + +.onb-input:focus { + border-color: var(--color-border-focus); + box-shadow: var(--shadow-focus); +} + +.onb-input::placeholder { + color: var(--color-text-muted); +} + +.onb-select { + height: 40px; + padding: 0 var(--space-3); + background: var(--color-bg-base); + border: 1px solid var(--color-border); + color: var(--color-text-primary); + font-family: var(--font-body); + font-size: var(--text-body-sm); + transition: var(--transition-fast); + outline: none; + cursor: pointer; +} + +[data-theme="industrial"] .onb-select { + border-radius: 0; +} + +[data-theme="modern"] .onb-select { + border-radius: var(--radius-md); +} + +.onb-select:focus { + border-color: var(--color-border-focus); + box-shadow: var(--shadow-focus); +} + +/* Inline error */ +.onb-error { + font-size: var(--text-caption); + color: var(--color-error); + min-height: 18px; + margin-top: var(--space-1); +} + +/* Success message in step */ +.onb-success { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-3); + background: var(--color-success-light); + color: var(--color-success-dark); + font-size: var(--text-body-sm); + font-weight: var(--font-weight-semibold); + margin-top: var(--space-3); +} + +[data-theme="industrial"] .onb-success { + border-radius: 0; +} + +[data-theme="modern"] .onb-success { + border-radius: var(--radius-md); +} + +/* Footer: buttons + progress */ +.onboarding-footer { + padding: var(--space-4) var(--space-6); + border-top: 1px solid var(--color-border); + display: flex; + flex-direction: column; + gap: var(--space-4); + background: var(--color-bg-elevated); +} + +.onb-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); +} + +/* Buttons */ +.onb-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: 0 var(--space-5); + height: 40px; + font-family: var(--font-body); + font-size: var(--text-body-sm); + font-weight: var(--font-weight-semibold); + border: 1px solid transparent; + cursor: pointer; + transition: var(--transition-fast); + white-space: nowrap; + letter-spacing: var(--tracking-wide); +} + +[data-theme="industrial"] .onb-btn { + border-radius: 0; + clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%); + text-transform: uppercase; +} + +[data-theme="modern"] .onb-btn { + border-radius: var(--radius-md); +} + +.onb-btn--primary { + background: var(--btn-primary-bg); + color: var(--btn-primary-text); +} + +.onb-btn--primary:hover { + background: var(--btn-primary-bg-hover); +} + +.onb-btn--primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.onb-btn--secondary { + background: var(--btn-secondary-bg); + color: var(--btn-secondary-text); + border-color: var(--btn-secondary-border); +} + +.onb-btn--secondary:hover { + background: var(--btn-secondary-bg-hover); +} + +.onb-btn--ghost { + background: transparent; + color: var(--color-text-muted); + border-color: transparent; +} + +.onb-btn--ghost:hover { + color: var(--color-text-primary); +} + +/* Progress dots */ +.onb-progress { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); +} + +.onb-dot { + width: 10px; + height: 10px; + border: 2px solid var(--color-border-strong); + background: transparent; + transition: var(--transition-normal); +} + +[data-theme="industrial"] .onb-dot { + border-radius: 0; +} + +[data-theme="modern"] .onb-dot { + border-radius: var(--radius-full); +} + +.onb-dot.is-active { + background: var(--color-primary); + border-color: var(--color-primary); + transform: scale(1.2); +} + +.onb-dot.is-done { + background: var(--color-primary-muted); + border-color: var(--color-primary); +} + +/* Step counter label */ +.onb-step-label { + font-size: var(--text-caption); + color: var(--color-text-muted); + text-align: center; + letter-spacing: var(--tracking-wide); +} + +/* "No mostrar de nuevo" checkbox row */ +.onb-dismiss-row { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding-top: var(--space-2); +} + +.onb-dismiss-row label { + font-size: var(--text-caption); + color: var(--color-text-muted); + cursor: pointer; +} + +.onb-dismiss-row input[type="checkbox"] { + accent-color: var(--color-primary); + cursor: pointer; +} + +/* Final step link grid */ +.onb-links { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--space-3); + margin-top: var(--space-4); +} + +.onb-link-card { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-2); + padding: var(--space-4) var(--space-3); + background: var(--color-bg-base); + border: 1px solid var(--color-border); + text-decoration: none; + color: var(--color-text-primary); + font-size: var(--text-body-sm); + font-weight: var(--font-weight-semibold); + transition: var(--transition-fast); +} + +[data-theme="industrial"] .onb-link-card { + border-radius: 0; +} + +[data-theme="modern"] .onb-link-card { + border-radius: var(--radius-md); +} + +.onb-link-card:hover { + border-color: var(--color-primary); + color: var(--color-primary); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.onb-link-card span.onb-link-icon { + font-size: 1.5rem; +} + +/* Step transition */ +.onb-step-enter { + animation: onb-step-in var(--duration-normal) var(--ease-out) forwards; +} + +@keyframes onb-step-in { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* Responsive */ +@media (max-width: 480px) { + .onboarding-modal { + width: 96vw; + max-height: 95vh; + } + .onboarding-body { + padding: var(--space-6) var(--space-4); + } + .onboarding-footer { + padding: var(--space-3) var(--space-4); + } + .onb-links { + grid-template-columns: 1fr; + } +} diff --git a/pos/static/js/onboarding.js b/pos/static/js/onboarding.js new file mode 100644 index 0000000..c757df4 --- /dev/null +++ b/pos/static/js/onboarding.js @@ -0,0 +1,449 @@ +/* ========================================================================== + NEXUS POS — Onboarding Wizard for New Tenants + Shows a step-by-step setup guide on first login. + Persists completion in localStorage('pos_onboarding_done'). + ========================================================================== */ + +(function () { + 'use strict'; + + /* ------------------------------------------------------------------ + GUARD — skip if already completed + ------------------------------------------------------------------ */ + + if (localStorage.getItem('pos_onboarding_done') === 'true') return; + + /* ------------------------------------------------------------------ + 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'); + 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(); + } + + /* Wait for DOM */ + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + +})(); diff --git a/pos/templates/catalog.html b/pos/templates/catalog.html index 1283f1c..31fe553 100644 --- a/pos/templates/catalog.html +++ b/pos/templates/catalog.html @@ -7,6 +7,7 @@ Catalogo — Nexus Autoparts POS + @@ -727,6 +728,7 @@ +