// noqa: js-006 - async init pattern is safe, loadData has try/catch // static/admin/js/store-roles.js /** * Admin store roles management page * * Super admins: merchant → store cascading selection. * Platform admins: store selection scoped to their platforms. * * Uses Tom Select for selection and permission catalog API for * displaying permissions with labels and descriptions. */ const storeRolesAdminLog = (window.LogConfig && window.LogConfig.createLogger) ? window.LogConfig.createLogger('adminStoreRoles', false) : console; storeRolesAdminLog.info('Loading...'); function adminStoreRoles() { storeRolesAdminLog.info('adminStoreRoles() called'); const config = window._adminStoreRolesConfig || {}; const isSuperAdmin = config.isSuperAdmin || false; return { // Inherit base layout state ...data(), // Set page identifier currentPage: 'store-roles', // Selection state isSuperAdmin, selectedMerchant: null, selectedStore: null, merchantSelector: null, storeSelector: null, // Role state loading: false, roles: [], rolesLoading: false, saving: false, showRoleModal: false, editingRole: null, roleForm: { name: '', permissions: [] }, permissionCategories: [], presetRoles: ['manager', 'staff', 'support', 'viewer', 'marketing'], async init() { // Guard against multiple initialization if (window._adminStoreRolesInitialized) { return; } window._adminStoreRolesInitialized = true; storeRolesAdminLog.info('Admin Store Roles init(), isSuperAdmin:', isSuperAdmin); this.$nextTick(() => { if (isSuperAdmin) { this.initMerchantSelector(); } else { this.initStoreSelector(); } }); // Load permission catalog await this.loadPermissionCatalog(); // Restore saved selection const savedStoreId = localStorage.getItem('admin_store_roles_selected_store_id'); if (savedStoreId) { storeRolesAdminLog.info('Restoring saved store:', savedStoreId); setTimeout(async () => { await this.restoreSavedStore(parseInt(savedStoreId)); }, 300); } storeRolesAdminLog.info('Admin Store Roles initialization complete'); }, // ===================================================================== // Permission Catalog // ===================================================================== async loadPermissionCatalog() { try { const response = await apiClient.get('/admin/store-roles/permissions/catalog'); this.permissionCategories = response.categories || []; storeRolesAdminLog.info('Loaded permission catalog:', this.permissionCategories.length, 'categories'); } catch (error) { storeRolesAdminLog.warn('Failed to load permission catalog:', error); } }, // ===================================================================== // Merchant Selector (Super Admin only) // ===================================================================== initMerchantSelector() { const el = this.$refs.merchantSelect; if (!el) { storeRolesAdminLog.warn('Merchant select element not found'); return; } const self = this; waitForTomSelect(() => { self.merchantSelector = new TomSelect(el, { valueField: 'id', labelField: 'name', searchField: ['name'], maxOptions: 50, placeholder: 'Search merchant by name...', load: async function(query, callback) { if (query.length < 2) { callback([]); return; } try { const response = await apiClient.get( `/admin/merchants?search=${encodeURIComponent(query)}&limit=50` ); const merchants = (response.merchants || []).map(m => ({ id: m.id, name: m.name, store_count: m.store_count || 0, })); callback(merchants); } catch (error) { storeRolesAdminLog.error('Merchant search failed:', error); callback([]); } }, render: { option: function(data, escape) { return `
${escape(data.name)} ${data.store_count} store(s)
`; }, item: function(data, escape) { return `
${escape(data.name)}
`; }, no_results: function() { return '
No merchants found
'; }, loading: function() { return '
Searching...
'; } }, onChange: function(value) { if (value) { const selected = this.options[value]; if (selected) { self.onMerchantSelected({ id: parseInt(value), name: selected.name, store_count: selected.store_count, }); } } else { self.onMerchantCleared(); } }, loadThrottle: 150, closeAfterSelect: true, persist: true, create: false, }); storeRolesAdminLog.info('Merchant selector initialized'); }); }, async onMerchantSelected(merchant) { storeRolesAdminLog.info('Merchant selected:', merchant.name); this.selectedMerchant = merchant; this.selectedStore = null; this.roles = []; localStorage.removeItem('admin_store_roles_selected_store_id'); // Destroy previous store selector and reinit with merchant filter if (this.storeSelector) { if (typeof this.storeSelector.destroy === 'function') { this.storeSelector.destroy(); } this.storeSelector = null; } // Wait for DOM update (x-show toggles the store select container) await this.$nextTick(); this.initStoreSelector(merchant.id); }, onMerchantCleared() { storeRolesAdminLog.info('Merchant cleared'); this.selectedMerchant = null; this.selectedStore = null; this.roles = []; localStorage.removeItem('admin_store_roles_selected_store_id'); if (this.storeSelector) { if (typeof this.storeSelector.destroy === 'function') { this.storeSelector.destroy(); } this.storeSelector = null; } }, // ===================================================================== // Store Selector // ===================================================================== initStoreSelector(merchantId = null) { const el = this.$refs.storeSelect; if (!el) { storeRolesAdminLog.warn('Store select element not found'); return; } const apiEndpoint = merchantId ? `/admin/stores?merchant_id=${merchantId}` : '/admin/stores'; this.storeSelector = initStoreSelector(el, { placeholder: merchantId ? 'Select store...' : 'Search store by name or code...', apiEndpoint: apiEndpoint, onSelect: async (store) => { storeRolesAdminLog.info('Store selected:', store); this.selectedStore = store; localStorage.setItem('admin_store_roles_selected_store_id', store.id.toString()); await this.loadRoles(); }, onClear: () => { storeRolesAdminLog.info('Store cleared'); this.selectedStore = null; this.roles = []; localStorage.removeItem('admin_store_roles_selected_store_id'); } }); }, // ===================================================================== // Restore / Clear // ===================================================================== async restoreSavedStore(storeId) { try { const store = await apiClient.get(`/admin/stores/${storeId}`); if (!store) return; if (isSuperAdmin && store.merchant_id) { // For super admin, restore the merchant first try { const merchant = await apiClient.get(`/admin/merchants/${store.merchant_id}`); if (merchant && this.merchantSelector) { this.merchantSelector.addOption({ id: merchant.id, name: merchant.name, store_count: merchant.store_count || 0, }); this.merchantSelector.setValue(merchant.id, true); this.selectedMerchant = { id: merchant.id, name: merchant.name }; // Wait for DOM, then init store selector and set value await this.$nextTick(); this.initStoreSelector(merchant.id); setTimeout(() => { if (this.storeSelector) { this.storeSelector.setValue(store.id, store); } this.selectedStore = { id: store.id, name: store.name, store_code: store.store_code }; this.loadRoles(); }, 300); } } catch (error) { storeRolesAdminLog.warn('Failed to restore merchant:', error); localStorage.removeItem('admin_store_roles_selected_store_id'); } } else { // Platform admin: just restore the store if (this.storeSelector) { this.storeSelector.setValue(store.id, store); } this.selectedStore = { id: store.id, name: store.name, store_code: store.store_code }; await this.loadRoles(); } storeRolesAdminLog.info('Restored store:', store.name); } catch (error) { storeRolesAdminLog.warn('Failed to restore saved store:', error); localStorage.removeItem('admin_store_roles_selected_store_id'); } }, clearSelection() { if (isSuperAdmin) { if (this.merchantSelector) { this.merchantSelector.clear(); } this.selectedMerchant = null; } if (this.storeSelector) { if (typeof this.storeSelector.clear === 'function') { this.storeSelector.clear(); } } this.selectedStore = null; this.roles = []; localStorage.removeItem('admin_store_roles_selected_store_id'); }, // ===================================================================== // Roles CRUD // ===================================================================== async loadRoles() { if (!this.selectedStore) return; this.rolesLoading = true; try { const response = await apiClient.get(`/admin/store-roles?store_id=${this.selectedStore.id}`); this.roles = response.roles || []; storeRolesAdminLog.info('Loaded', this.roles.length, 'roles'); } catch (error) { storeRolesAdminLog.error('Failed to load roles:', error); Utils.showToast(error.message || 'Failed to load roles', 'error'); } finally { this.rolesLoading = 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 = category.permissions || []; 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 = category.permissions || []; return perms.length > 0 && perms.every(p => this.roleForm.permissions.includes(p.id)); }, async saveRole() { if (!this.selectedStore) return; this.saving = true; try { const storeParam = `store_id=${this.selectedStore.id}`; if (this.editingRole) { await apiClient.put(`/admin/store-roles/${this.editingRole.id}?${storeParam}`, this.roleForm); } else { await apiClient.post(`/admin/store-roles?${storeParam}`, this.roleForm); } this.showRoleModal = false; Utils.showToast('Role saved successfully', 'success'); await this.loadRoles(); } catch (error) { storeRolesAdminLog.error('Error saving role:', error); Utils.showToast(error.message || 'Failed to save role', 'error'); } finally { this.saving = false; } }, async confirmDelete(role) { if (!this.selectedStore) return; if (!confirm(`Delete role "${role.name}"? This cannot be undone.`)) return; try { await apiClient.delete(`/admin/store-roles/${role.id}?store_id=${this.selectedStore.id}`); Utils.showToast('Role deleted successfully', 'success'); await this.loadRoles(); } catch (error) { storeRolesAdminLog.error('Error deleting role:', error); Utils.showToast(error.message || 'Failed to delete role', 'error'); } }, }; } storeRolesAdminLog.info('Module loaded');