feat: multi-module improvements across merchant, store, i18n, and customer systems
All checks were successful
All checks were successful
- Fix platform-grouped merchant sidebar menu with core items at root level - Add merchant store management (detail page, create store, team page) - Fix store settings 500 error by removing dead stripe/API tab - Move onboarding translations to module-owned locale files - Fix onboarding banner i18n with server-side rendering + context inheritance - Refactor login language selectors to use languageSelector() function (LANG-002) - Move HTTPException handling to global exception handler in merchant routes (API-003) - Add language selector to all login pages and portal headers - Fix customer module: drop order stats from customer model, add to orders module - Fix admin menu config visibility for super admin platform context - Fix storefront auth and layout issues - Add missing i18n translations for onboarding steps (en/fr/de/lb) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,6 @@ Provides menu visibility configuration for admin and store frontends:
|
||||
- GET /menu-config/platforms/{platform_id} - Get menu config for a platform
|
||||
- PUT /menu-config/platforms/{platform_id} - Update menu visibility for a platform
|
||||
- POST /menu-config/platforms/{platform_id}/reset - Reset to defaults
|
||||
- GET /menu-config/user - Get current user's menu config (super admins)
|
||||
- PUT /menu-config/user - Update current user's menu config (super admins)
|
||||
- GET /menu/admin - Get rendered admin menu for current user
|
||||
- GET /menu/store - Get rendered store menu for current platform
|
||||
|
||||
@@ -316,108 +314,6 @@ async def reset_platform_menu_config(
|
||||
return {"success": True, "message": "Menu configuration reset to defaults"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# User Menu Configuration (Super Admin Only)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/user", response_model=MenuConfigResponse)
|
||||
async def get_user_menu_config(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Get the current super admin's personal menu configuration.
|
||||
|
||||
Only super admins can configure their own admin menu.
|
||||
"""
|
||||
items = menu_service.get_user_menu_config(db, current_user.id)
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} fetched their personal menu config"
|
||||
)
|
||||
|
||||
# Use user's preferred language, falling back to middleware-resolved language
|
||||
language = current_user.preferred_language or getattr(
|
||||
request.state, "language", "en"
|
||||
)
|
||||
|
||||
return _build_menu_config_response(
|
||||
items, FrontendType.ADMIN, language=language, user_id=current_user.id
|
||||
)
|
||||
|
||||
|
||||
@router.put("/user")
|
||||
async def update_user_menu_visibility(
|
||||
update_data: MenuVisibilityUpdateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Update visibility for a single menu item for the current super admin.
|
||||
|
||||
Super admin only. Cannot hide mandatory items.
|
||||
"""
|
||||
menu_service.update_menu_visibility(
|
||||
db=db,
|
||||
frontend_type=FrontendType.ADMIN,
|
||||
menu_item_id=update_data.menu_item_id,
|
||||
is_visible=update_data.is_visible,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} updated personal menu: "
|
||||
f"{update_data.menu_item_id}={update_data.is_visible}"
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Menu visibility updated"}
|
||||
|
||||
|
||||
@router.post("/user/reset", response_model=MenuActionResponse)
|
||||
async def reset_user_menu_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Reset the current super admin's menu configuration (hide all except mandatory).
|
||||
|
||||
Super admin only.
|
||||
"""
|
||||
menu_service.reset_user_menu_config(db, current_user.id)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} reset their personal menu config (hide all)"
|
||||
)
|
||||
|
||||
return MenuActionResponse(
|
||||
success=True, message="Menu configuration reset - all items hidden"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/user/show-all", response_model=MenuActionResponse)
|
||||
async def show_all_user_menu_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Show all menu items for the current super admin.
|
||||
|
||||
Super admin only.
|
||||
"""
|
||||
menu_service.show_all_user_menu_config(db, current_user.id)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} enabled all menu items"
|
||||
)
|
||||
|
||||
return MenuActionResponse(success=True, message="All menu items are now visible")
|
||||
|
||||
|
||||
@router.post("/platforms/{platform_id}/show-all")
|
||||
async def show_all_platform_menu_config(
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
@@ -467,11 +363,12 @@ async def get_rendered_admin_menu(
|
||||
Used by the frontend to render the sidebar.
|
||||
"""
|
||||
if current_user.is_super_admin:
|
||||
# Super admin: use user-level config
|
||||
# Super admin: use platform config if platform selected, else global (all modules)
|
||||
platform_id = current_user.token_platform_id
|
||||
menu = menu_service.get_menu_for_rendering(
|
||||
db=db,
|
||||
frontend_type=FrontendType.ADMIN,
|
||||
user_id=current_user.id,
|
||||
platform_id=platform_id,
|
||||
is_super_admin=True,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -5,9 +5,11 @@ Merchant menu rendering endpoint.
|
||||
Provides the dynamic sidebar menu for the merchant portal:
|
||||
- GET /menu/render/merchant - Get rendered merchant menu for current user
|
||||
|
||||
Menu sections are driven by module definitions (FrontendType.MERCHANT).
|
||||
Only modules enabled on platforms the merchant is actively subscribed to
|
||||
will appear in the sidebar.
|
||||
Menu sections are grouped by platform:
|
||||
- Core items (dashboard, billing, account) appear at root level
|
||||
- Platform-specific items are grouped under their platform name
|
||||
- No AdminMenuConfig visibility filtering — menu is purely driven by
|
||||
module definitions + module enablement + subscription status
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -38,6 +40,9 @@ class MenuSectionResponse(BaseModel):
|
||||
|
||||
id: str
|
||||
label: str | None = None
|
||||
icon: str | None = None
|
||||
platform_code: str | None = None
|
||||
is_collapsible: bool = False
|
||||
items: list[dict[str, Any]]
|
||||
|
||||
|
||||
@@ -83,10 +88,12 @@ async def get_rendered_merchant_menu(
|
||||
"""
|
||||
Get the rendered merchant menu for the current user.
|
||||
|
||||
Returns the filtered menu structure based on modules enabled
|
||||
on platforms the merchant is subscribed to.
|
||||
Returns a platform-grouped menu structure:
|
||||
- Core sections (dashboard, billing, account) at root level
|
||||
- Platform-specific sections grouped under platform name
|
||||
|
||||
Used by the merchant frontend to render the sidebar dynamically.
|
||||
Menu visibility is driven by module definitions + module enablement
|
||||
+ subscription status. No AdminMenuConfig filtering.
|
||||
"""
|
||||
# Resolve the merchant for this user (via service layer)
|
||||
merchant = menu_service.get_merchant_for_menu(db, current_user.id)
|
||||
@@ -98,32 +105,25 @@ async def get_rendered_merchant_menu(
|
||||
sections=[],
|
||||
)
|
||||
|
||||
# Get union of enabled module codes across all subscribed platforms
|
||||
enabled_codes = menu_service.get_merchant_enabled_module_codes(db, merchant.id)
|
||||
|
||||
# Resolve primary platform for AdminMenuConfig visibility lookup
|
||||
primary_platform_id = menu_service.get_merchant_primary_platform_id(
|
||||
# Get platform-grouped menu
|
||||
core_sections, platform_sections = menu_service.get_merchant_menu_by_platform(
|
||||
db, merchant.id
|
||||
)
|
||||
|
||||
# Get filtered menu using enabled_module_codes override + platform visibility
|
||||
menu = menu_service.get_menu_for_rendering(
|
||||
db=db,
|
||||
frontend_type=FrontendType.MERCHANT,
|
||||
platform_id=primary_platform_id,
|
||||
enabled_module_codes=enabled_codes,
|
||||
)
|
||||
|
||||
# Resolve language
|
||||
language = current_user.preferred_language or getattr(
|
||||
request.state, "language", "en"
|
||||
)
|
||||
|
||||
# Translate section and item labels
|
||||
sections = []
|
||||
for section in menu:
|
||||
# Build response sections
|
||||
all_sections = []
|
||||
|
||||
# Core sections first (translated labels)
|
||||
for section in core_sections:
|
||||
translated_items = []
|
||||
for item in section.items:
|
||||
if not item.is_module_enabled:
|
||||
continue
|
||||
translated_items.append(
|
||||
{
|
||||
"id": item.id,
|
||||
@@ -133,15 +133,59 @@ async def get_rendered_merchant_menu(
|
||||
}
|
||||
)
|
||||
|
||||
sections.append(
|
||||
MenuSectionResponse(
|
||||
id=section.id,
|
||||
label=_translate_label(section.label_key, language),
|
||||
items=translated_items,
|
||||
if translated_items:
|
||||
all_sections.append(
|
||||
MenuSectionResponse(
|
||||
id=section.id,
|
||||
label=_translate_label(section.label_key, language),
|
||||
icon=section.icon,
|
||||
is_collapsible=section.is_collapsible,
|
||||
items=translated_items,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Platform sections (platform name as label, collapsible)
|
||||
for section in platform_sections:
|
||||
translated_items = []
|
||||
for item in section.items:
|
||||
if not item.is_module_enabled:
|
||||
continue
|
||||
translated_items.append(
|
||||
{
|
||||
"id": item.id,
|
||||
"label": _translate_label(item.label_key, language),
|
||||
"icon": item.icon,
|
||||
"url": item.route,
|
||||
}
|
||||
)
|
||||
|
||||
if translated_items:
|
||||
# Extract platform_code from section id (format: "platform-{code}")
|
||||
platform_code = section.id.removeprefix("platform-")
|
||||
all_sections.append(
|
||||
MenuSectionResponse(
|
||||
id=section.id,
|
||||
label=section.label_key, # Already platform name, not a translation key
|
||||
icon=section.icon,
|
||||
platform_code=platform_code,
|
||||
is_collapsible=True,
|
||||
items=translated_items,
|
||||
)
|
||||
)
|
||||
|
||||
# Sort order: 1) Dashboard (main), 2) Platform sections, 3) Billing, 4) Account
|
||||
# Core sections use their definition order; platform sections slot in between
|
||||
# main=0, platform=25, billing=50, account=900
|
||||
def _section_sort_key(s):
|
||||
if s.platform_code:
|
||||
return 25, s.id
|
||||
# Map known core section IDs to their display order
|
||||
core_order = {"main": 0, "billing": 50, "account": 900}
|
||||
return core_order.get(s.id, 500), s.id
|
||||
|
||||
all_sections.sort(key=_section_sort_key)
|
||||
|
||||
return RenderedMenuResponse(
|
||||
frontend_type=FrontendType.MERCHANT.value,
|
||||
sections=sections,
|
||||
sections=all_sections,
|
||||
)
|
||||
|
||||
@@ -224,14 +224,6 @@ def get_store_settings(
|
||||
"is_verified": domain.is_verified,
|
||||
})
|
||||
|
||||
# Get Stripe info from subscription (read-only, masked)
|
||||
stripe_info = None
|
||||
if store.subscription and store.subscription.stripe_customer_id:
|
||||
stripe_info = {
|
||||
"has_stripe_customer": True,
|
||||
"customer_id_masked": f"cus_***{store.subscription.stripe_customer_id[-4:]}",
|
||||
}
|
||||
|
||||
return {
|
||||
# General info
|
||||
"store_code": store.store_code,
|
||||
@@ -297,9 +289,6 @@ def get_store_settings(
|
||||
"domains": domains,
|
||||
"default_subdomain": f"{store.subdomain}.letzshop.lu",
|
||||
|
||||
# Stripe info (read-only)
|
||||
"stripe_info": stripe_info,
|
||||
|
||||
# Options for dropdowns
|
||||
"options": {
|
||||
"supported_languages": SUPPORTED_LANGUAGES,
|
||||
|
||||
Reference in New Issue
Block a user