// 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');