/** * Alpine.js global stores for the CRM frontend. * * Stores: * - $store.auth: current user, JWT, isAuthenticated, login/logout helpers * - $store.notifications: toast queue (success/error/info/warning) * * Pages access these as $store.auth.user, $store.notifications.success(msg), etc. * * IMPORTANT: This file is loaded as a module AFTER Alpine's CDN script. The * `alpine:init` event fires once Alpine is ready, at which point we register * the stores. Until then, $store.auth etc. are undefined. * * R-5: never use x-html. All user-derived strings go through x-text or * interpolation in templates. */ import { api, setToken, clearToken, getToken } from './api.js'; import { login as authLogin, register as authRegister, logout as authLogout, refreshToken as authRefresh, fetchCurrentUser as authFetchMe } from './auth.js'; document.addEventListener('alpine:init', () => { // === AUTH STORE ========================================================= Alpine.store('auth', { token: getToken(), user: null, isAuthenticated: !!getToken(), loading: false, error: null, /** Called from x-init on protected pages */ async init() { if (!this.isAuthenticated) { // Auth-gate: redirect to login window.location.href = '/index.html'; return; } await this.fetchUser(); }, async fetchUser() { this.loading = true; this.error = null; try { this.user = await authFetchMe(); } catch (err) { // 401 is already handled by api.js (token cleared, redirect). // For other errors, store them and log out. this.error = err.message || 'Failed to load user'; await this.logout(); } finally { this.loading = false; } }, async login(email, password) { this.loading = true; this.error = null; try { await authLogin(email, password); this.token = getToken(); this.isAuthenticated = true; await this.fetchUser(); } catch (err) { this.error = err.message || 'Login failed'; throw err; } finally { this.loading = false; } }, async register(payload) { this.loading = true; this.error = null; try { await authRegister(payload); this.token = getToken(); this.isAuthenticated = true; await this.fetchUser(); } catch (err) { this.error = err.message || 'Registration failed'; throw err; } finally { this.loading = false; } }, async logout() { try { await authLogout(); } catch { // best-effort } finally { this.token = null; this.user = null; this.isAuthenticated = false; clearToken(); } }, async refresh() { try { await authRefresh(); this.token = getToken(); } catch (err) { this.error = err.message || 'Token refresh failed'; await this.logout(); } }, /** Convenience: is the current user an admin? */ get isAdmin() { return this.user && this.user.role === 'admin'; }, }); // === NOTIFICATIONS STORE ================================================ Alpine.store('notifications', { items: [], /** * Push a new toast. * @param {string} message * @param {'info'|'success'|'error'|'warning'} [type='info'] * @param {number} [timeout=5000] ms before auto-dismiss (0 = no auto-dismiss) */ push(message, type = 'info', timeout = 5000) { const id = Date.now() + '-' + Math.random().toString(36).slice(2, 8); this.items.push({ id, message: String(message), type }); if (timeout > 0) { setTimeout(() => this.dismiss(id), timeout); } return id; }, dismiss(id) { this.items = this.items.filter(n => n.id !== id); }, success(msg, timeout) { return this.push(msg, 'success', timeout); }, error(msg, timeout) { return this.push(msg, 'error', timeout); }, info(msg, timeout) { return this.push(msg, 'info', timeout); }, warning(msg, timeout) { return this.push(msg, 'warning', timeout); }, }); }); // Expose a tiny global helper so non-module code (e.g. inline page handlers) // can call notifications without an explicit import. window.crmNotify = (msg, type) => { if (window.Alpine && window.Alpine.store('notifications')) { return window.Alpine.store('notifications').push(msg, type || 'info'); } console.warn('crmNotify called before Alpine was ready:', msg); };