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