/** * Store Roles Management — Alpine.js component * * Provides CRUD for custom roles with a permission matrix UI. * Loaded on /store/{store_code}/team/roles. */ const storeRolesLog = createModuleLogger?.('storeRoles') ?? console; function storeRoles() { storeRolesLog.info('storeRoles() called'); return { // Inherit base layout state ...data(), // Set page identifier currentPage: 'team', // Component state roles: [], loading: true, error: false, saving: false, showRoleModal: false, editingRole: null, roleForm: { name: '', permissions: [] }, permissionsByCategory: {}, presetRoles: ['manager', 'staff', 'support', 'viewer', 'marketing'], async init() { // Guard against multiple initialization if (window._storeRolesInitialized) { storeRolesLog.warn('Already initialized, skipping'); return; } window._storeRolesInitialized = true; // Call parent init to set storeCode from URL const parentInit = data().init; if (parentInit) { await parentInit.call(this); } await this.loadPermissions(); await this.loadRoles(); }, async loadPermissions() { try { // Group known permissions by category prefix const allPerms = window.USER_PERMISSIONS || []; this.permissionsByCategory = this.groupPermissions(allPerms); } catch (e) { storeRolesLog.warn('Could not load permission categories:', e); } }, groupPermissions(permIds) { // Known permission categories from the codebase const knownPerms = [ 'dashboard.view', 'settings.view', 'settings.edit', 'settings.theme', 'settings.domains', 'products.view', 'products.create', 'products.edit', 'products.delete', 'products.import', 'products.export', 'orders.view', 'orders.edit', 'orders.cancel', 'orders.refund', 'customers.view', 'customers.edit', 'customers.delete', 'customers.export', 'stock.view', 'stock.edit', 'stock.transfer', 'team.view', 'team.invite', 'team.edit', 'team.remove', 'analytics.view', 'analytics.export', 'messaging.view_messages', 'messaging.send_messages', 'messaging.manage_templates', 'billing.view_tiers', 'billing.manage_tiers', 'billing.view_subscriptions', 'billing.manage_subscriptions', 'billing.view_invoices', 'cms.view_pages', 'cms.manage_pages', 'cms.view_media', 'cms.manage_media', 'cms.manage_themes', 'loyalty.view_programs', 'loyalty.manage_programs', 'loyalty.view_rewards', 'loyalty.manage_rewards', 'cart.view', 'cart.manage', ]; const groups = {}; for (const perm of knownPerms) { const cat = perm.split('.')[0]; if (!groups[cat]) groups[cat] = []; groups[cat].push({ id: perm }); } return groups; }, async loadRoles() { this.loading = true; this.error = false; try { const response = await apiClient.get('/store/team/roles'); this.roles = response.roles || []; } catch (e) { this.error = true; storeRolesLog.error('Error loading roles:', e); } finally { this.loading = false; } }, isPresetRole(name) { return this.presetRoles.includes(name.toLowerCase()); }, openCreateModal() { this.editingRole = null; this.roleForm = { name: '', permissions: [] }; this.showRoleModal = true; }, openEditModal(role) { this.editingRole = role; this.roleForm = { name: role.name, permissions: [...(role.permissions || [])], }; this.showRoleModal = true; }, togglePermission(permId) { const idx = this.roleForm.permissions.indexOf(permId); if (idx >= 0) { this.roleForm.permissions.splice(idx, 1); } else { this.roleForm.permissions.push(permId); } }, toggleCategory(category) { const perms = this.permissionsByCategory[category] || []; const permIds = perms.map(p => p.id); const allSelected = permIds.every(id => this.roleForm.permissions.includes(id)); if (allSelected) { this.roleForm.permissions = this.roleForm.permissions.filter(id => !permIds.includes(id)); } else { for (const id of permIds) { if (!this.roleForm.permissions.includes(id)) { this.roleForm.permissions.push(id); } } } }, isCategoryFullySelected(category) { const perms = this.permissionsByCategory[category] || []; return perms.length > 0 && perms.every(p => this.roleForm.permissions.includes(p.id)); }, async saveRole() { this.saving = true; try { if (this.editingRole) { await apiClient.put(`/store/team/roles/${this.editingRole.id}`, this.roleForm); } else { await apiClient.post('/store/team/roles', this.roleForm); } this.showRoleModal = false; Utils.showToast('Role saved successfully', 'success'); await this.loadRoles(); } catch (e) { storeRolesLog.error('Error saving role:', e); Utils.showToast(e.message || 'Failed to save role', 'error'); } finally { this.saving = false; } }, async confirmDelete(role) { if (!confirm(`Delete role "${role.name}"? This cannot be undone.`)) return; try { await apiClient.delete(`/store/team/roles/${role.id}`); Utils.showToast('Role deleted successfully', 'success'); await this.loadRoles(); } catch (e) { storeRolesLog.error('Error deleting role:', e); Utils.showToast(e.message || 'Failed to delete role', 'error'); } }, }; }