Files
Autoparts-DB/pos/static/js/onboarding.js
2026-04-04 02:15:01 +00:00

450 lines
15 KiB
JavaScript

/* ==========================================================================
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();
}
})();