- Add AdminMenuConfig model for per-platform menu customization - Add menu registry for centralized menu configuration - Add my-menu-config and platform-menu-config admin pages - Update sidebar with improved layout and Alpine.js interactions - Add FrontendType enum for admin/vendor menu separation - Document self-contained module patterns in session note Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
428 lines
16 KiB
JavaScript
428 lines
16 KiB
JavaScript
/**
|
|
* Alpine.js v3 global data initialization
|
|
* Provides theme toggle, menu controls, sidebar sections, and page state
|
|
*/
|
|
function data() {
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Theme (dark mode) persistence
|
|
// ─────────────────────────────────────────────────────────────────
|
|
function getThemeFromLocalStorage() {
|
|
if (window.localStorage.getItem('dark')) {
|
|
return JSON.parse(window.localStorage.getItem('dark'))
|
|
}
|
|
return (
|
|
!!window.matchMedia &&
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
)
|
|
}
|
|
|
|
function setThemeToLocalStorage(value) {
|
|
window.localStorage.setItem('dark', value)
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Sidebar sections persistence
|
|
// ─────────────────────────────────────────────────────────────────
|
|
const SIDEBAR_STORAGE_KEY = 'admin_sidebar_sections';
|
|
|
|
// Default state: Platform Administration open, others closed
|
|
const defaultSections = {
|
|
superAdmin: true, // Super admin section (only visible to super admins)
|
|
platformAdmin: true,
|
|
vendorOps: false,
|
|
marketplace: false,
|
|
billing: false,
|
|
contentMgmt: false,
|
|
devTools: false,
|
|
platformHealth: false,
|
|
monitoring: false,
|
|
settingsSection: false
|
|
};
|
|
|
|
function getSidebarSectionsFromStorage() {
|
|
try {
|
|
const stored = window.localStorage.getItem(SIDEBAR_STORAGE_KEY);
|
|
if (stored) {
|
|
return { ...defaultSections, ...JSON.parse(stored) };
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to parse sidebar sections from localStorage:', e);
|
|
}
|
|
return { ...defaultSections };
|
|
}
|
|
|
|
function saveSidebarSectionsToStorage(sections) {
|
|
try {
|
|
window.localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(sections));
|
|
} catch (e) {
|
|
console.warn('Failed to save sidebar sections to localStorage:', e);
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Last visited page tracking (for redirect after login)
|
|
// ─────────────────────────────────────────────────────────────────
|
|
const LAST_PAGE_KEY = 'admin_last_visited_page';
|
|
const currentPath = window.location.pathname;
|
|
|
|
// Save current page (exclude login, logout, error pages)
|
|
if (currentPath.startsWith('/admin/') &&
|
|
!currentPath.includes('/login') &&
|
|
!currentPath.includes('/logout') &&
|
|
!currentPath.includes('/errors/')) {
|
|
try {
|
|
window.localStorage.setItem(LAST_PAGE_KEY, currentPath);
|
|
} catch (e) {
|
|
// Ignore storage errors
|
|
}
|
|
}
|
|
|
|
// Helper to get admin profile from localStorage
|
|
function getAdminProfileFromStorage() {
|
|
try {
|
|
// Check admin_user first (set by login), then adminProfile (legacy)
|
|
const stored = window.localStorage.getItem('admin_user') ||
|
|
window.localStorage.getItem('adminProfile');
|
|
if (stored) {
|
|
return JSON.parse(stored);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to parse admin profile from localStorage:', e);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Map pages to their parent sections
|
|
const pageSectionMap = {
|
|
// Super Admin section
|
|
'admin-users': 'superAdmin',
|
|
// Platform Administration
|
|
companies: 'platformAdmin',
|
|
vendors: 'platformAdmin',
|
|
messages: 'platformAdmin',
|
|
// Vendor Operations (Products, Customers, Inventory, Orders, Shipping)
|
|
'marketplace-products': 'vendorOps',
|
|
'vendor-products': 'vendorOps',
|
|
customers: 'vendorOps',
|
|
inventory: 'vendorOps',
|
|
orders: 'vendorOps',
|
|
// Future: shipping will map to 'vendorOps'
|
|
// Marketplace
|
|
'marketplace-letzshop': 'marketplace',
|
|
// Content Management
|
|
'platform-homepage': 'contentMgmt',
|
|
'content-pages': 'contentMgmt',
|
|
'vendor-theme': 'contentMgmt',
|
|
// Developer Tools
|
|
components: 'devTools',
|
|
icons: 'devTools',
|
|
// Platform Health
|
|
testing: 'platformHealth',
|
|
'code-quality': 'platformHealth',
|
|
// Platform Monitoring
|
|
imports: 'monitoring',
|
|
'background-tasks': 'monitoring',
|
|
logs: 'monitoring',
|
|
'notifications-settings': 'monitoring',
|
|
// Platform Settings
|
|
settings: 'settingsSection',
|
|
profile: 'settingsSection',
|
|
'api-keys': 'settingsSection'
|
|
};
|
|
|
|
return {
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Theme
|
|
// ─────────────────────────────────────────────────────────────────
|
|
dark: getThemeFromLocalStorage(),
|
|
toggleTheme() {
|
|
this.dark = !this.dark
|
|
setThemeToLocalStorage(this.dark)
|
|
},
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Mobile side menu
|
|
// ─────────────────────────────────────────────────────────────────
|
|
isSideMenuOpen: false,
|
|
toggleSideMenu() {
|
|
this.isSideMenuOpen = !this.isSideMenuOpen
|
|
},
|
|
closeSideMenu() {
|
|
this.isSideMenuOpen = false
|
|
},
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Notifications menu
|
|
// ─────────────────────────────────────────────────────────────────
|
|
isNotificationsMenuOpen: false,
|
|
toggleNotificationsMenu() {
|
|
this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen
|
|
},
|
|
closeNotificationsMenu() {
|
|
this.isNotificationsMenuOpen = false
|
|
},
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Profile menu
|
|
// ─────────────────────────────────────────────────────────────────
|
|
isProfileMenuOpen: false,
|
|
toggleProfileMenu() {
|
|
this.isProfileMenuOpen = !this.isProfileMenuOpen
|
|
},
|
|
closeProfileMenu() {
|
|
this.isProfileMenuOpen = false
|
|
},
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Pages menu (legacy)
|
|
// ─────────────────────────────────────────────────────────────────
|
|
isPagesMenuOpen: false,
|
|
togglePagesMenu() {
|
|
this.isPagesMenuOpen = !this.isPagesMenuOpen
|
|
},
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Collapsible sidebar sections
|
|
// ─────────────────────────────────────────────────────────────────
|
|
openSections: getSidebarSectionsFromStorage(),
|
|
|
|
toggleSection(section) {
|
|
this.openSections[section] = !this.openSections[section];
|
|
saveSidebarSectionsToStorage(this.openSections);
|
|
},
|
|
|
|
// Auto-expand section containing current page
|
|
expandSectionForCurrentPage() {
|
|
const section = pageSectionMap[this.currentPage];
|
|
if (section && !this.openSections[section]) {
|
|
this.openSections[section] = true;
|
|
saveSidebarSectionsToStorage(this.openSections);
|
|
}
|
|
},
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Page identifier - will be set by individual pages
|
|
// ─────────────────────────────────────────────────────────────────
|
|
currentPage: '',
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Admin profile and super admin flag
|
|
// ─────────────────────────────────────────────────────────────────
|
|
adminProfile: getAdminProfileFromStorage(),
|
|
|
|
get isSuperAdmin() {
|
|
return this.adminProfile?.is_super_admin === true;
|
|
},
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Dynamic menu visibility (loaded from API)
|
|
// ─────────────────────────────────────────────────────────────────
|
|
menuData: null,
|
|
menuLoading: false,
|
|
visibleMenuItems: new Set(),
|
|
|
|
async loadMenuConfig(forceReload = false) {
|
|
// Don't reload if already loaded (unless forced)
|
|
if (!forceReload && (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('admin_token')) {
|
|
console.debug('Menu config: no admin_token, skipping');
|
|
return;
|
|
}
|
|
|
|
this.menuLoading = true;
|
|
try {
|
|
this.menuData = await apiClient.get('/admin/menu-config/render/admin');
|
|
// Build a set of visible menu item IDs for quick lookup
|
|
this.visibleMenuItems = new Set();
|
|
for (const section of (this.menuData?.sections || [])) {
|
|
for (const item of (section.items || [])) {
|
|
this.visibleMenuItems.add(item.id);
|
|
}
|
|
}
|
|
console.debug('Menu config loaded:', this.visibleMenuItems.size, 'items');
|
|
} catch (e) {
|
|
// Silently fail - menu will show all items as fallback
|
|
console.debug('Menu config not loaded, using defaults:', e?.message || e);
|
|
} finally {
|
|
this.menuLoading = false;
|
|
}
|
|
},
|
|
|
|
async reloadSidebarMenu() {
|
|
// Force reload the sidebar menu config
|
|
this.menuData = null;
|
|
this.visibleMenuItems = new Set();
|
|
await this.loadMenuConfig(true);
|
|
},
|
|
|
|
isMenuItemVisible(menuItemId) {
|
|
// If menu not loaded yet, show all items (fallback to hardcoded)
|
|
if (!this.menuData) return true;
|
|
return this.visibleMenuItems.has(menuItemId);
|
|
},
|
|
|
|
isSectionVisible(sectionId) {
|
|
// If menu not loaded yet, show all sections
|
|
if (!this.menuData) return true;
|
|
// Check if any item in this section is visible
|
|
const section = this.menuData?.sections?.find(s => s.id === sectionId);
|
|
return section && section.items && section.items.length > 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Language selector component for i18n support
|
|
* Used by language_selector macros in templates
|
|
*
|
|
* @param {string} currentLang - Current language code (e.g., 'fr')
|
|
* @param {Array} enabledLanguages - Array of enabled language codes
|
|
* @returns {Object} Alpine.js component data
|
|
*/
|
|
function languageSelector(currentLang, enabledLanguages) {
|
|
return {
|
|
isLangOpen: false,
|
|
currentLang: currentLang || 'fr',
|
|
languages: enabledLanguages || ['fr', 'de', 'en'],
|
|
languageNames: {
|
|
'en': 'English',
|
|
'fr': 'Français',
|
|
'de': 'Deutsch',
|
|
'lb': 'Lëtzebuergesch'
|
|
},
|
|
languageFlags: {
|
|
'en': 'gb',
|
|
'fr': 'fr',
|
|
'de': 'de',
|
|
'lb': 'lu'
|
|
},
|
|
async setLanguage(lang) {
|
|
if (lang === this.currentLang) {
|
|
this.isLangOpen = false;
|
|
return;
|
|
}
|
|
try {
|
|
const response = await fetch('/api/v1/language/set', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ language: lang })
|
|
});
|
|
if (response.ok) {
|
|
this.currentLang = lang;
|
|
window.location.reload();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to set language:', error);
|
|
}
|
|
this.isLangOpen = false;
|
|
}
|
|
};
|
|
}
|
|
|
|
// Export to window for use in templates
|
|
window.languageSelector = languageSelector;
|
|
|
|
/**
|
|
* Header messages badge component
|
|
* Shows unread message count in header
|
|
*/
|
|
function headerMessages() {
|
|
return {
|
|
unreadCount: 0,
|
|
pollInterval: null,
|
|
|
|
async init() {
|
|
await this.fetchUnreadCount();
|
|
// Poll every 30 seconds
|
|
this.pollInterval = setInterval(() => this.fetchUnreadCount(), 30000);
|
|
},
|
|
|
|
destroy() {
|
|
if (this.pollInterval) {
|
|
clearInterval(this.pollInterval);
|
|
}
|
|
},
|
|
|
|
async fetchUnreadCount() {
|
|
try {
|
|
const response = await apiClient.get('/admin/messages/unread-count');
|
|
this.unreadCount = response.total_unread || 0;
|
|
} catch (error) {
|
|
// Silently fail - don't spam console for auth issues on login page
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Export to window
|
|
window.headerMessages = headerMessages;
|
|
|
|
/**
|
|
* Platform Settings Utility
|
|
* Provides cached access to platform-wide settings
|
|
*/
|
|
const PlatformSettings = {
|
|
// Cache key and TTL
|
|
CACHE_KEY: 'platform_settings_cache',
|
|
CACHE_TTL: 5 * 60 * 1000, // 5 minutes
|
|
|
|
/**
|
|
* Get cached settings or fetch from API
|
|
*/
|
|
async get() {
|
|
try {
|
|
const cached = localStorage.getItem(this.CACHE_KEY);
|
|
if (cached) {
|
|
const { data, timestamp } = JSON.parse(cached);
|
|
if (Date.now() - timestamp < this.CACHE_TTL) {
|
|
return data;
|
|
}
|
|
}
|
|
|
|
// Fetch from API
|
|
const response = await apiClient.get('/admin/settings/display/public');
|
|
const settings = {
|
|
rows_per_page: response.rows_per_page || 20
|
|
};
|
|
|
|
// Cache the result
|
|
localStorage.setItem(this.CACHE_KEY, JSON.stringify({
|
|
data: settings,
|
|
timestamp: Date.now()
|
|
}));
|
|
|
|
return settings;
|
|
} catch (error) {
|
|
console.warn('Failed to load platform settings, using defaults:', error);
|
|
return { rows_per_page: 20 };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get rows per page setting
|
|
*/
|
|
async getRowsPerPage() {
|
|
const settings = await this.get();
|
|
return settings.rows_per_page;
|
|
},
|
|
|
|
/**
|
|
* Clear the cache (call after saving settings)
|
|
*/
|
|
clearCache() {
|
|
localStorage.removeItem(this.CACHE_KEY);
|
|
}
|
|
};
|
|
|
|
// Export to window
|
|
window.PlatformSettings = PlatformSettings; |