feat(pos): onboarding wizard para nuevos tenants
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
428
pos/static/css/onboarding.css
Normal file
428
pos/static/css/onboarding.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
449
pos/static/js/onboarding.js
Normal file
449
pos/static/js/onboarding.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Catalogo — Nexus Autoparts POS</title>
|
<title>Catalogo — Nexus Autoparts POS</title>
|
||||||
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
|
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
|
||||||
<link rel="stylesheet" href="/pos/static/css/chat.css" />
|
<link rel="stylesheet" href="/pos/static/css/chat.css" />
|
||||||
|
<link rel="stylesheet" href="/pos/static/css/onboarding.css" />
|
||||||
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
|
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
|
||||||
<meta name="theme-color" content="#F5A623" />
|
<meta name="theme-color" content="#F5A623" />
|
||||||
|
|
||||||
@@ -727,6 +728,7 @@
|
|||||||
<script src="/pos/static/js/offline-banner.js"></script>
|
<script src="/pos/static/js/offline-banner.js"></script>
|
||||||
<script src="/pos/static/js/chat.js"></script>
|
<script src="/pos/static/js/chat.js"></script>
|
||||||
<script src="/pos/static/js/sync-engine.js"></script>
|
<script src="/pos/static/js/sync-engine.js"></script>
|
||||||
|
<script src="/pos/static/js/onboarding.js"></script>
|
||||||
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user