diff --git a/app/modules/core/routes/api/admin_menu_config.py b/app/modules/core/routes/api/admin_menu_config.py index 13711769..4463545c 100644 --- a/app/modules/core/routes/api/admin_menu_config.py +++ b/app/modules/core/routes/api/admin_menu_config.py @@ -30,6 +30,7 @@ from app.api.deps import ( from app.modules.core.services.menu_service import MenuItemConfig, menu_service from app.modules.tenancy.services.platform_service import platform_service from app.modules.enums import FrontendType # noqa: API-007 - Enum for type safety +from app.utils.i18n import translate, DEFAULT_LANGUAGE from models.schema.auth import UserContext logger = logging.getLogger(__name__) @@ -111,15 +112,39 @@ class MenuActionResponse(BaseModel): # ============================================================================= -def _build_menu_item_response(item: MenuItemConfig) -> MenuItemResponse: - """Convert MenuItemConfig to API response.""" +def _translate_label(label_key: str | None, language: str) -> str | None: + """ + Translate a label key to the user's language. + + Falls back to a readable version of the key if translation not found. + """ + if not label_key: + return None + + translated = translate(label_key, language=language) + + # If translation returned the key itself, it wasn't found + # Create a readable fallback from the last part of the key + if translated == label_key: + parts = label_key.split(".") + last_part = parts[-1] + # Convert snake_case to Title Case + return last_part.replace("_", " ").title() + + return translated + + +def _build_menu_item_response( + item: MenuItemConfig, language: str = DEFAULT_LANGUAGE +) -> MenuItemResponse: + """Convert MenuItemConfig to API response with translated labels.""" return MenuItemResponse( id=item.id, - label=item.label, + label=_translate_label(item.label, language), icon=item.icon, url=item.url, section_id=item.section_id, - section_label=item.section_label, + section_label=_translate_label(item.section_label, language), is_visible=item.is_visible, is_mandatory=item.is_mandatory, is_super_admin_only=item.is_super_admin_only, @@ -129,11 +154,12 @@ def _build_menu_item_response(item: MenuItemConfig) -> MenuItemResponse: def _build_menu_config_response( items: list[MenuItemConfig], frontend_type: FrontendType, + language: str = DEFAULT_LANGUAGE, platform_id: int | None = None, user_id: int | None = None, ) -> MenuConfigResponse: - """Build menu configuration response.""" - item_responses = [_build_menu_item_response(item) for item in items] + """Build menu configuration response with translated labels.""" + item_responses = [_build_menu_item_response(item, language) for item in items] visible_count = sum(1 for item in items if item.is_visible) return MenuConfigResponse( @@ -177,7 +203,12 @@ async def get_platform_menu_config( f"for platform {platform.code} ({frontend_type.value})" ) - return _build_menu_config_response(items, frontend_type, platform_id=platform_id) + # Use user's preferred language, falling back to default + language = current_user.preferred_language or DEFAULT_LANGUAGE + + return _build_menu_config_response( + items, frontend_type, language=language, platform_id=platform_id + ) @router.put("/platforms/{platform_id}") @@ -300,8 +331,11 @@ async def get_user_menu_config( f"[MENU_CONFIG] Super admin {current_user.email} fetched their personal menu config" ) + # Use user's preferred language, falling back to default + language = current_user.preferred_language or DEFAULT_LANGUAGE + return _build_menu_config_response( - items, FrontendType.ADMIN, user_id=current_user.id + items, FrontendType.ADMIN, language=language, user_id=current_user.id ) @@ -448,14 +482,26 @@ async def get_rendered_admin_menu( is_super_admin=False, ) - sections = [ - MenuSectionResponse( - id=section["id"], - label=section.get("label"), - items=section["items"], + # Use user's preferred language, falling back to default + language = current_user.preferred_language or DEFAULT_LANGUAGE + + # Translate section and item labels + sections = [] + for section in menu.get("sections", []): + # Translate item labels + translated_items = [] + for item in section.get("items", []): + translated_item = item.copy() + translated_item["label"] = _translate_label(item.get("label"), language) + translated_items.append(translated_item) + + sections.append( + MenuSectionResponse( + id=section["id"], + label=_translate_label(section.get("label"), language), + items=translated_items, + ) ) - for section in menu.get("sections", []) - ] return RenderedMenuResponse( frontend_type=FrontendType.ADMIN.value, diff --git a/app/modules/core/static/admin/js/init-alpine.js b/app/modules/core/static/admin/js/init-alpine.js index d9b83661..226c9bf1 100644 --- a/app/modules/core/static/admin/js/init-alpine.js +++ b/app/modules/core/static/admin/js/init-alpine.js @@ -22,33 +22,21 @@ function data() { // ───────────────────────────────────────────────────────────────── // Sidebar sections persistence + // Dynamic - section IDs come from menu discovery API // ───────────────────────────────────────────────────────────────── 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) }; + return JSON.parse(stored); } } catch (e) { console.warn('Failed to parse sidebar sections from localStorage:', e); } - return { ...defaultSections }; + // Default: first section open (will be set dynamically) + return {}; } function saveSidebarSectionsToStorage(sections) { @@ -92,43 +80,18 @@ function data() { 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' - }; + // 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; + } return { // ───────────────────────────────────────────────────────────────── @@ -191,9 +154,10 @@ function data() { saveSidebarSectionsToStorage(this.openSections); }, - // Auto-expand section containing current page + // Auto-expand section containing current page (uses menu API data) expandSectionForCurrentPage() { - const section = pageSectionMap[this.currentPage]; + if (!this.menuData) return; + const section = findSectionForPage(this.menuData, this.currentPage); if (section && !this.openSections[section]) { this.openSections[section] = true; saveSidebarSectionsToStorage(this.openSections); @@ -242,12 +206,21 @@ function data() { 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 || [])) { + const sections = this.menuData?.sections || []; + for (const section of sections) { for (const item of (section.items || [])) { this.visibleMenuItems.add(item.id); } + // Initialize openSections for new sections (default: first section open) + if (this.openSections[section.id] === undefined) { + // Default: first section open, rest closed + this.openSections[section.id] = (sections.indexOf(section) === 0); + } } console.debug('Menu config loaded:', this.visibleMenuItems.size, 'items'); + + // Auto-expand section containing current page + this.expandSectionForCurrentPage(); } catch (e) { // Silently fail - menu will show all items as fallback console.debug('Menu config not loaded, using defaults:', e?.message || e); diff --git a/app/modules/core/static/admin/js/my-menu-config.js b/app/modules/core/static/admin/js/my-menu-config.js index dc7749b6..e43a8655 100644 --- a/app/modules/core/static/admin/js/my-menu-config.js +++ b/app/modules/core/static/admin/js/my-menu-config.js @@ -57,9 +57,6 @@ function adminMyMenuConfig() { }, async init() { - // Load i18n translations - await I18n.loadModule('core'); - // Guard against multiple initialization if (window._adminMyMenuConfigInitialized) { myMenuConfigLog.warn('Already initialized, skipping'); @@ -70,6 +67,8 @@ function adminMyMenuConfig() { myMenuConfigLog.info('=== MY MENU CONFIG PAGE INITIALIZING ==='); try { + // Load core translations for confirmations + await I18n.loadModule('core'); await this.loadMenuConfig(); myMenuConfigLog.info('=== MY MENU CONFIG PAGE INITIALIZED ==='); } catch (error) { diff --git a/app/modules/marketplace/locales/de.json b/app/modules/marketplace/locales/de.json index 9a5e0b3a..39fc2204 100644 --- a/app/modules/marketplace/locales/de.json +++ b/app/modules/marketplace/locales/de.json @@ -1,4 +1,12 @@ { + "menu": { + "marketplace": "Marktplatz", + "letzshop": "Letzshop", + "products_inventory": "Produkte & Inventar", + "marketplace_import": "Marktplatz Import", + "sales_orders": "Verkäufe & Bestellungen", + "letzshop_orders": "Letzshop Bestellungen" + }, "marketplace": { "title": "Marktplatz", "import": "Importieren", diff --git a/app/modules/marketplace/locales/en.json b/app/modules/marketplace/locales/en.json index 3ef75f53..03540562 100644 --- a/app/modules/marketplace/locales/en.json +++ b/app/modules/marketplace/locales/en.json @@ -1,4 +1,12 @@ { + "menu": { + "marketplace": "Marketplace", + "letzshop": "Letzshop", + "products_inventory": "Products & Inventory", + "marketplace_import": "Marketplace Import", + "sales_orders": "Sales & Orders", + "letzshop_orders": "Letzshop Orders" + }, "marketplace": { "title": "Marketplace", "import": "Import", diff --git a/app/modules/marketplace/locales/fr.json b/app/modules/marketplace/locales/fr.json index 9afd6e4f..ca56f8d2 100644 --- a/app/modules/marketplace/locales/fr.json +++ b/app/modules/marketplace/locales/fr.json @@ -1,4 +1,12 @@ { + "menu": { + "marketplace": "Marketplace", + "letzshop": "Letzshop", + "products_inventory": "Produits et Inventaire", + "marketplace_import": "Import Marketplace", + "sales_orders": "Ventes et Commandes", + "letzshop_orders": "Commandes Letzshop" + }, "marketplace": { "title": "Marketplace", "import": "Importer", diff --git a/app/modules/marketplace/locales/lb.json b/app/modules/marketplace/locales/lb.json index 5c3d434c..1fd847e1 100644 --- a/app/modules/marketplace/locales/lb.json +++ b/app/modules/marketplace/locales/lb.json @@ -1,4 +1,12 @@ { + "menu": { + "marketplace": "Marchéplaz", + "letzshop": "Letzshop", + "products_inventory": "Produkter & Inventar", + "marketplace_import": "Marchéplaz Import", + "sales_orders": "Verkaf & Bestellungen", + "letzshop_orders": "Letzshop Bestellungen" + }, "marketplace": { "title": "Marchéplaz", "import": "Import", diff --git a/app/modules/payments/locales/de.json b/app/modules/payments/locales/de.json index 90f49116..d347014a 100644 --- a/app/modules/payments/locales/de.json +++ b/app/modules/payments/locales/de.json @@ -1,12 +1,12 @@ { + "menu": { + "payments": "Zahlungen" + }, "payments": { - "title": "Zahlungen", - "menu": { - "payments": "Zahlungen" - }, - "messages": { - "payment_successful": "Zahlung erfolgreich verarbeitet", - "payment_failed": "Zahlungsverarbeitung fehlgeschlagen" - } + "title": "Zahlungen" + }, + "messages": { + "payment_successful": "Zahlung erfolgreich verarbeitet", + "payment_failed": "Zahlungsverarbeitung fehlgeschlagen" } } diff --git a/app/modules/payments/locales/en.json b/app/modules/payments/locales/en.json index 8a83ad34..c42a43da 100644 --- a/app/modules/payments/locales/en.json +++ b/app/modules/payments/locales/en.json @@ -1,12 +1,12 @@ { + "menu": { + "payments": "Payments" + }, "payments": { - "title": "Payments", - "menu": { - "payments": "Payments" - }, - "messages": { - "payment_successful": "Payment processed successfully", - "payment_failed": "Payment processing failed" - } + "title": "Payments" + }, + "messages": { + "payment_successful": "Payment processed successfully", + "payment_failed": "Payment processing failed" } } diff --git a/app/modules/payments/locales/fr.json b/app/modules/payments/locales/fr.json index 3ee1a6c7..09e67775 100644 --- a/app/modules/payments/locales/fr.json +++ b/app/modules/payments/locales/fr.json @@ -1,12 +1,12 @@ { + "menu": { + "payments": "Paiements" + }, "payments": { - "title": "Paiements", - "menu": { - "payments": "Paiements" - }, - "messages": { - "payment_successful": "Paiement traité avec succès", - "payment_failed": "Échec du traitement du paiement" - } + "title": "Paiements" + }, + "messages": { + "payment_successful": "Paiement traité avec succès", + "payment_failed": "Échec du traitement du paiement" } } diff --git a/app/modules/payments/locales/lb.json b/app/modules/payments/locales/lb.json index 2ed07269..77369e8c 100644 --- a/app/modules/payments/locales/lb.json +++ b/app/modules/payments/locales/lb.json @@ -1,12 +1,12 @@ { + "menu": { + "payments": "Bezuelungen" + }, "payments": { - "title": "Bezuelungen", - "menu": { - "payments": "Bezuelungen" - }, - "messages": { - "payment_successful": "Bezuelung erfollegräich veraarbecht", - "payment_failed": "Bezuelungsveraarbechtung ass feelgeschloen" - } + "title": "Bezuelungen" + }, + "messages": { + "payment_successful": "Bezuelung erfollegräich veraarbecht", + "payment_failed": "Bezuelungsveraarbechtung ass feelgeschloen" } } diff --git a/app/modules/tenancy/static/admin/js/platform-menu-config.js b/app/modules/tenancy/static/admin/js/platform-menu-config.js index d202cd9b..2c3195dd 100644 --- a/app/modules/tenancy/static/admin/js/platform-menu-config.js +++ b/app/modules/tenancy/static/admin/js/platform-menu-config.js @@ -52,9 +52,6 @@ function adminPlatformMenuConfig(platformCode) { }, async init() { - // Load i18n translations - await I18n.loadModule('tenancy'); - // Guard against duplicate initialization if (window._platformMenuConfigInitialized) { menuConfigLog.warn('Already initialized, skipping'); @@ -66,6 +63,8 @@ function adminPlatformMenuConfig(platformCode) { menuConfigLog.info('Platform code:', this.platformCode); try { + // Load tenancy translations for confirmations + await I18n.loadModule('tenancy'); await this.loadPlatform(); await this.loadPlatformMenuConfig(); menuConfigLog.info('=== PLATFORM MENU CONFIG PAGE INITIALIZED ==='); diff --git a/app/templates/admin/partials/sidebar.html b/app/templates/admin/partials/sidebar.html index 4d6ef4d4..9ca7a027 100644 --- a/app/templates/admin/partials/sidebar.html +++ b/app/templates/admin/partials/sidebar.html @@ -1,55 +1,5 @@ {# app/templates/admin/partials/sidebar.html #} -{# Collapsible sidebar sections with localStorage persistence #} - -{# ============================================================================ - REUSABLE MACROS FOR SIDEBAR ITEMS - ============================================================================ #} - -{# Macro for collapsible section header #} -{% macro section_header(title, section_key) %} -
-
-
- -{% endmacro %} - -{# Macro for collapsible section content wrapper #} -{% macro section_content(section_key) %} - -{% endmacro %} - -{# Macro for menu item with visibility check #} -{% macro menu_item(page_id, url, icon, label) %} -
  • - - - - {{ label }} - -
  • -{% endmacro %} +{# Dynamic sidebar that renders from menu discovery API #} {# ============================================================================ SIDEBAR CONTENT (shared between desktop and mobile) @@ -61,125 +11,84 @@ Admin Portal - - - - - - - -
    - {{ section_header('Platform Administration', 'platformAdmin') }} - {% call section_content('platformAdmin') %} - {{ menu_item('companies', '/admin/companies', 'office-building', 'Companies') }} - {{ menu_item('vendors', '/admin/vendors', 'shopping-bag', 'Vendors') }} - {{ menu_item('messages', '/admin/messages', 'chat-bubble-left-right', 'Messages') }} - {% endcall %}
    - -
    - {{ section_header('Vendor Operations', 'vendorOps') }} - {% call section_content('vendorOps') %} - {{ menu_item('vendor-products', '/admin/vendor-products', 'cube', 'Products') }} - {{ menu_item('customers', '/admin/customers', 'user-group', 'Customers') }} - {{ menu_item('inventory', '/admin/inventory', 'archive', 'Inventory') }} - {{ menu_item('orders', '/admin/orders', 'clipboard-list', 'Orders') }} - {# Future items - uncomment when implemented: - {{ menu_item('shipping', '/admin/shipping', 'truck', 'Shipping') }} - #} - {% endcall %} + +
    + +
    - -
    - {{ section_header('Marketplace', 'marketplace') }} - {% call section_content('marketplace') %} - {{ menu_item('marketplace-letzshop', '/admin/marketplace/letzshop', 'shopping-cart', 'Letzshop') }} - {% endcall %} -
    - - -
    - {{ section_header('Billing & Subscriptions', 'billing') }} - {% call section_content('billing') %} - {{ menu_item('subscription-tiers', '/admin/subscription-tiers', 'tag', 'Subscription Tiers') }} - {{ menu_item('subscriptions', '/admin/subscriptions', 'credit-card', 'Vendor Subscriptions') }} - {{ menu_item('billing-history', '/admin/billing-history', 'document-text', 'Billing History') }} - {% endcall %} -
    - - -
    - {{ section_header('Content Management', 'contentMgmt') }} - {% call section_content('contentMgmt') %} - {{ menu_item('platforms', '/admin/platforms', 'globe-alt', 'Platforms') }} - {{ menu_item('content-pages', '/admin/content-pages', 'document-text', 'Content Pages') }} - {{ menu_item('vendor-themes', '/admin/vendor-themes', 'color-swatch', 'Vendor Themes') }} - {% endcall %} -
    - - -
    - {{ section_header('Developer Tools', 'devTools') }} - {% call section_content('devTools') %} - {{ menu_item('components', '/admin/components', 'view-grid', 'Components') }} - {{ menu_item('icons', '/admin/icons', 'photograph', 'Icons') }} - {% endcall %} -
    - - -
    - {{ section_header('Platform Health', 'platformHealth') }} - {% call section_content('platformHealth') %} - {{ menu_item('platform-health', '/admin/platform-health', 'chart-bar', 'Capacity Monitor') }} - {{ menu_item('testing', '/admin/testing', 'beaker', 'Testing Hub') }} - {{ menu_item('code-quality', '/admin/code-quality', 'shield-check', 'Code Quality') }} - {% endcall %} -
    - - -
    - {{ section_header('Platform Monitoring', 'monitoring') }} - {% call section_content('monitoring') %} - {{ menu_item('imports', '/admin/imports', 'cube', 'Import Jobs') }} - {{ menu_item('background-tasks', '/admin/background-tasks', 'collection', 'Background Tasks') }} - {{ menu_item('logs', '/admin/logs', 'document-text', 'Application Logs') }} - {{ menu_item('notifications', '/admin/notifications', 'bell', 'Notifications') }} - {% endcall %} -
    - - -
    - {{ section_header('Platform Settings', 'settings') }} - {% call section_content('settings') %} - {{ menu_item('settings', '/admin/settings', 'cog', 'General') }} - {{ menu_item('email-templates', '/admin/email-templates', 'mail', 'Email Templates') }} - - - {# TODO: Implement profile and API keys pages #} - {# {{ menu_item('profile', '/admin/profile', 'user-circle', 'Profile') }} #} - {# {{ menu_item('api-keys', '/admin/api-keys', 'key', 'API Keys') }} #} - {% endcall %} + +
    + +

    Loading menu...

    {% endmacro %}