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 %}