From c82210795faba5bc0598d5e4b76d13608464340f Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Thu, 26 Feb 2026 19:15:19 +0100 Subject: [PATCH] fix: move storeRoles() to external JS with base layout inheritance The inline storeRoles() was missing ...data() spread, causing Alpine errors for dark mode, sidebar, storeCode etc. Follow the same pattern as team.js: external JS file with ...data() and parent init() call. Uses apiClient and Utils.showToast per architecture rules. Co-Authored-By: Claude Opus 4.6 --- app/modules/tenancy/static/store/js/roles.js | 179 +++++++++++++++++ .../templates/tenancy/store/roles.html | 186 +----------------- 2 files changed, 180 insertions(+), 185 deletions(-) create mode 100644 app/modules/tenancy/static/store/js/roles.js diff --git a/app/modules/tenancy/static/store/js/roles.js b/app/modules/tenancy/static/store/js/roles.js new file mode 100644 index 00000000..123bd7da --- /dev/null +++ b/app/modules/tenancy/static/store/js/roles.js @@ -0,0 +1,179 @@ +/** + * 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'); + } + }, + }; +} diff --git a/app/modules/tenancy/templates/tenancy/store/roles.html b/app/modules/tenancy/templates/tenancy/store/roles.html index 1a65dac6..83a0817c 100644 --- a/app/modules/tenancy/templates/tenancy/store/roles.html +++ b/app/modules/tenancy/templates/tenancy/store/roles.html @@ -150,189 +150,5 @@ {% endblock %} {% block extra_scripts %} - + {% endblock %}