diff --git a/pos/app.py b/pos/app.py index 453be2e..0442560 100644 --- a/pos/app.py +++ b/pos/app.py @@ -15,6 +15,16 @@ def create_app(): def health(): return {'status': 'ok'} + from flask import render_template, send_from_directory + + @app.route('/pos/login') + def pos_login(): + return render_template('login.html') + + @app.route('/pos/static/') + def pos_static(filename): + return send_from_directory('static', filename) + return app if __name__ == '__main__': diff --git a/pos/static/css/common.css b/pos/static/css/common.css new file mode 100644 index 0000000..86caa36 --- /dev/null +++ b/pos/static/css/common.css @@ -0,0 +1,57 @@ +/* /home/Autopartes/pos/static/css/common.css */ +/* Theme variables — overridden by tenant theme */ +:root { + --color-primary: #1a73e8; + --color-secondary: #5f6368; + --color-accent: #ff6b35; + --color-bg: #ffffff; + --color-surface: #f8f9fa; + --color-text: #202124; + --color-text-secondary: #5f6368; + --color-border: #dadce0; + --color-success: #34a853; + --color-warning: #f9ab00; + --color-error: #ea4335; + --font-display: 'Sora', sans-serif; + --font-body: 'Plus Jakarta Sans', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + --radius: 8px; + --shadow: 0 1px 3px rgba(0,0,0,0.12); +} + +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: var(--font-body); + background: var(--color-bg); + color: var(--color-text); + line-height: 1.6; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 10px 20px; + border: 1px solid var(--color-border); + border-radius: var(--radius); + font-family: var(--font-body); + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + background: var(--color-surface); + color: var(--color-text); +} + +.btn:hover { background: var(--color-border); } +.btn--primary { background: var(--color-primary); color: white; border-color: var(--color-primary); } +.btn--primary:hover { opacity: 0.9; } +.btn--accent { background: var(--color-accent); color: white; border-color: var(--color-accent); } + +.card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 24px; +} diff --git a/pos/static/js/login.js b/pos/static/js/login.js new file mode 100644 index 0000000..8dc887b --- /dev/null +++ b/pos/static/js/login.js @@ -0,0 +1,111 @@ +// /home/Autopartes/pos/static/js/login.js +(function() { + 'use strict'; + + var pin = ''; + var dots = document.querySelectorAll('#pinDots .pin-dot'); + var errorEl = document.getElementById('loginError'); + + // Get tenant_id from URL param or localStorage + var tenantId = new URLSearchParams(window.location.search).get('tenant') + || localStorage.getItem('pos_tenant_id'); + + // Device ID (persistent) + var deviceId = localStorage.getItem('pos_device_id'); + if (!deviceId) { + deviceId = 'dev-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); + localStorage.setItem('pos_device_id', deviceId); + } + + /** + * Check if a JWT token is expired by decoding its payload. + * Returns true if the token is valid (not expired), false otherwise. + */ + function isTokenValid(token) { + try { + var parts = token.split('.'); + if (parts.length !== 3) return false; + // Base64url decode the payload (index 1) + var payload = parts[1].replace(/-/g, '+').replace(/_/g, '/'); + var decoded = JSON.parse(atob(payload)); + // exp is in seconds, Date.now() is in milliseconds + if (!decoded.exp) return false; + // Add 30-second buffer to avoid edge cases + return (decoded.exp * 1000) > (Date.now() + 30000); + } catch (e) { + return false; + } + } + + function updateDots() { + dots.forEach(function(dot, i) { + dot.classList.toggle('filled', i < pin.length); + }); + } + + window.addDigit = function(d) { + if (pin.length >= 4) return; + pin += d; + updateDots(); + errorEl.textContent = ''; + if (pin.length === 4) { + submitPin(); + } + }; + + window.clearPin = function() { + pin = ''; + updateDots(); + errorEl.textContent = ''; + }; + + window.submitPin = function() { + if (pin.length !== 4) return; + errorEl.textContent = ''; + + fetch('/pos/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tenant_id: parseInt(tenantId), + pin: pin, + device_id: deviceId + }) + }) + .then(function(res) { return res.json().then(function(d) { return { ok: res.ok, data: d }; }); }) + .then(function(result) { + if (!result.ok) { + errorEl.textContent = result.data.error || 'Error de autenticacion'; + clearPin(); + return; + } + localStorage.setItem('pos_token', result.data.token); + localStorage.setItem('pos_employee', JSON.stringify(result.data.employee)); + localStorage.setItem('pos_tenant_id', tenantId); + window.location.href = '/pos/catalog'; + }) + .catch(function() { + errorEl.textContent = 'Error de conexion'; + clearPin(); + }); + }; + + // Keyboard support + document.addEventListener('keydown', function(e) { + if (e.key >= '0' && e.key <= '9') addDigit(e.key); + else if (e.key === 'Backspace') clearPin(); + else if (e.key === 'Enter') submitPin(); + }); + + // Auto-redirect if already logged in AND token is not expired + var token = localStorage.getItem('pos_token'); + if (token && tenantId) { + if (isTokenValid(token)) { + window.location.href = '/pos/catalog'; + } else { + // Token expired — clean up and stay on login page + localStorage.removeItem('pos_token'); + localStorage.removeItem('pos_employee'); + } + } +})(); diff --git a/pos/templates/login.html b/pos/templates/login.html new file mode 100644 index 0000000..e592fa9 --- /dev/null +++ b/pos/templates/login.html @@ -0,0 +1,53 @@ + + + + + + + Nexus POS — Login + + + + +
+ + +
+
+
+
+
+
+
+ + + + + + + + + + + + +
+ +
+ + +