feat: dynamic merchant sidebar with module-driven menus
Replace the hardcoded merchant sidebar with a dynamic menu system driven by module definitions, matching the existing admin frontend pattern. Modules declare FrontendType.MERCHANT menus in their definition.py, and a new API endpoint unions enabled modules across all platforms the merchant is subscribed to — so loyalty only appears when enabled. - Add MERCHANT menu definitions to core, billing, tenancy, loyalty modules - Extend MenuDiscoveryService with enabled_module_codes parameter - Create GET /merchants/core/menu/render/merchant endpoint - Update merchant Alpine.js with loadMenuConfig() and dynamic section state - Replace hardcoded sidebar.html with x-for rendering + loading skeleton + fallback - Add 36 unit and integration tests for menu discovery, service, and endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,11 +21,8 @@ function getMerchantSidebarSectionsFromStorage() {
|
||||
} catch (e) {
|
||||
console.warn('[MERCHANT INIT-ALPINE] Failed to load sidebar state from localStorage:', e);
|
||||
}
|
||||
// Default: all sections open
|
||||
return {
|
||||
billing: true,
|
||||
account: true
|
||||
};
|
||||
// Default: empty — dynamic menu will initialize section state
|
||||
return {};
|
||||
}
|
||||
|
||||
function saveMerchantSidebarSectionsToStorage(sections) {
|
||||
@@ -36,6 +33,19 @@ function saveMerchantSidebarSectionsToStorage(sections) {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to find section ID for a page from menu data
|
||||
function findSectionForPage(menuData, pageId) {
|
||||
if (!menuData?.sections) return null;
|
||||
for (const section of menuData.sections) {
|
||||
for (const item of (section.items || [])) {
|
||||
if (item.id === pageId) {
|
||||
return section.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function data() {
|
||||
console.log('[MERCHANT INIT-ALPINE] data() function called');
|
||||
return {
|
||||
@@ -48,6 +58,10 @@ function data() {
|
||||
// Sidebar collapsible sections state
|
||||
openSections: getMerchantSidebarSectionsFromStorage(),
|
||||
|
||||
// Dynamic menu (loaded from API)
|
||||
menuData: null,
|
||||
menuLoading: false,
|
||||
|
||||
init() {
|
||||
// Set current page from URL
|
||||
const path = window.location.pathname;
|
||||
@@ -87,6 +101,9 @@ function data() {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
// Load dynamic menu
|
||||
this.loadMenuConfig();
|
||||
},
|
||||
|
||||
toggleSideMenu() {
|
||||
@@ -116,6 +133,54 @@ function data() {
|
||||
saveMerchantSidebarSectionsToStorage(this.openSections);
|
||||
},
|
||||
|
||||
// Auto-expand section containing current page
|
||||
expandSectionForCurrentPage() {
|
||||
if (!this.menuData) return;
|
||||
const section = findSectionForPage(this.menuData, this.currentPage);
|
||||
if (section && !this.openSections[section]) {
|
||||
this.openSections[section] = true;
|
||||
saveMerchantSidebarSectionsToStorage(this.openSections);
|
||||
}
|
||||
},
|
||||
|
||||
// Dynamic menu loading from API
|
||||
async loadMenuConfig() {
|
||||
if (this.menuData || this.menuLoading) return;
|
||||
|
||||
// Skip if apiClient is not available (e.g., on login page)
|
||||
if (typeof apiClient === 'undefined') {
|
||||
console.debug('Menu config: apiClient not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if not authenticated
|
||||
if (!localStorage.getItem('merchant_token')) {
|
||||
console.debug('Menu config: no merchant_token, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuLoading = true;
|
||||
try {
|
||||
this.menuData = await apiClient.get('/merchants/core/menu/render/merchant');
|
||||
const sections = this.menuData?.sections || [];
|
||||
for (const section of sections) {
|
||||
// Initialize openSections for new sections (default: open)
|
||||
if (this.openSections[section.id] === undefined) {
|
||||
this.openSections[section.id] = true;
|
||||
}
|
||||
}
|
||||
saveMerchantSidebarSectionsToStorage(this.openSections);
|
||||
console.debug('Menu config loaded:', sections.length, 'sections');
|
||||
|
||||
// Auto-expand section containing current page
|
||||
this.expandSectionForCurrentPage();
|
||||
} catch (e) {
|
||||
console.debug('Menu config not loaded, using fallback:', e?.message || e);
|
||||
} finally {
|
||||
this.menuLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async handleLogout() {
|
||||
console.log('Logging out merchant user...');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user