# app/api/v1/admin/menu_config.py """ Admin API endpoints for Platform Menu Configuration. Provides menu visibility configuration for admin and vendor 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/vendor - Get rendered vendor menu for current platform All configuration endpoints require super admin access. Menu rendering endpoints require authenticated admin/vendor access. """ import logging from typing import Any from fastapi import APIRouter, Depends, Path, Query from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.api.deps import ( get_current_admin_from_cookie_or_header, get_current_super_admin, get_db, ) from app.services.menu_service import MenuItemConfig, menu_service from app.services.platform_service import platform_service from models.database.admin_menu_config import FrontendType from models.database.user import User logger = logging.getLogger(__name__) router = APIRouter(prefix="/menu-config") # ============================================================================= # Pydantic Schemas # ============================================================================= class MenuItemResponse(BaseModel): """Menu item configuration response.""" id: str label: str icon: str url: str section_id: str section_label: str | None = None is_visible: bool = True is_mandatory: bool = False is_super_admin_only: bool = False class MenuConfigResponse(BaseModel): """Menu configuration response for a platform or user.""" frontend_type: str platform_id: int | None = None user_id: int | None = None items: list[MenuItemResponse] total_items: int visible_items: int hidden_items: int class MenuVisibilityUpdateRequest(BaseModel): """Request to update menu item visibility.""" menu_item_id: str = Field(..., description="Menu item ID to update") is_visible: bool = Field(..., description="Whether the item should be visible") class BulkMenuVisibilityUpdateRequest(BaseModel): """Request to update multiple menu items at once.""" visibility: dict[str, bool] = Field( ..., description="Map of menu_item_id to is_visible", examples=[{"inventory": False, "orders": True}], ) class MenuSectionResponse(BaseModel): """Menu section for rendering.""" id: str label: str | None = None items: list[dict[str, Any]] class RenderedMenuResponse(BaseModel): """Rendered menu for frontend.""" frontend_type: str sections: list[MenuSectionResponse] class MenuActionResponse(BaseModel): """Response for menu action operations (reset, show-all, etc.).""" success: bool message: str # ============================================================================= # Helper Functions # ============================================================================= def _build_menu_item_response(item: MenuItemConfig) -> MenuItemResponse: """Convert MenuItemConfig to API response.""" return MenuItemResponse( id=item.id, label=item.label, icon=item.icon, url=item.url, section_id=item.section_id, section_label=item.section_label, is_visible=item.is_visible, is_mandatory=item.is_mandatory, is_super_admin_only=item.is_super_admin_only, ) def _build_menu_config_response( items: list[MenuItemConfig], frontend_type: FrontendType, 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] visible_count = sum(1 for item in items if item.is_visible) return MenuConfigResponse( frontend_type=frontend_type.value, platform_id=platform_id, user_id=user_id, items=item_responses, total_items=len(items), visible_items=visible_count, hidden_items=len(items) - visible_count, ) # ============================================================================= # Platform Menu Configuration (Super Admin Only) # ============================================================================= @router.get("/platforms/{platform_id}", response_model=MenuConfigResponse) async def get_platform_menu_config( platform_id: int = Path(..., description="Platform ID"), frontend_type: FrontendType = Query( FrontendType.ADMIN, description="Frontend type (admin or vendor)" ), db: Session = Depends(get_db), current_user: User = Depends(get_current_super_admin), ): """ Get menu configuration for a platform. Returns all menu items with their visibility status for the specified platform and frontend type. Super admin only. """ # Verify platform exists platform = platform_service.get_platform_by_id(db, platform_id) items = menu_service.get_platform_menu_config(db, frontend_type, platform_id) logger.info( f"[MENU_CONFIG] Super admin {current_user.email} fetched menu config " f"for platform {platform.code} ({frontend_type.value})" ) return _build_menu_config_response(items, frontend_type, platform_id=platform_id) @router.put("/platforms/{platform_id}") async def update_platform_menu_visibility( update_data: MenuVisibilityUpdateRequest, platform_id: int = Path(..., description="Platform ID"), frontend_type: FrontendType = Query( FrontendType.ADMIN, description="Frontend type (admin or vendor)" ), db: Session = Depends(get_db), current_user: User = Depends(get_current_super_admin), ): """ Update visibility for a single menu item for a platform. Super admin only. Cannot hide mandatory items. """ # Verify platform exists platform = platform_service.get_platform_by_id(db, platform_id) menu_service.update_menu_visibility( db=db, frontend_type=frontend_type, menu_item_id=update_data.menu_item_id, is_visible=update_data.is_visible, platform_id=platform_id, ) db.commit() logger.info( f"[MENU_CONFIG] Super admin {current_user.email} updated menu visibility: " f"{update_data.menu_item_id}={update_data.is_visible} " f"for platform {platform.code} ({frontend_type.value})" ) return {"success": True, "message": "Menu visibility updated"} @router.put("/platforms/{platform_id}/bulk") async def bulk_update_platform_menu_visibility( update_data: BulkMenuVisibilityUpdateRequest, platform_id: int = Path(..., description="Platform ID"), frontend_type: FrontendType = Query( FrontendType.ADMIN, description="Frontend type (admin or vendor)" ), db: Session = Depends(get_db), current_user: User = Depends(get_current_super_admin), ): """ Update visibility for multiple menu items at once. Super admin only. Skips mandatory items silently. """ # Verify platform exists platform = platform_service.get_platform_by_id(db, platform_id) menu_service.bulk_update_menu_visibility( db=db, frontend_type=frontend_type, visibility_map=update_data.visibility, platform_id=platform_id, ) db.commit() logger.info( f"[MENU_CONFIG] Super admin {current_user.email} bulk updated menu visibility: " f"{len(update_data.visibility)} items for platform {platform.code} ({frontend_type.value})" ) return {"success": True, "message": f"Updated {len(update_data.visibility)} menu items"} @router.post("/platforms/{platform_id}/reset") async def reset_platform_menu_config( platform_id: int = Path(..., description="Platform ID"), frontend_type: FrontendType = Query( FrontendType.ADMIN, description="Frontend type (admin or vendor)" ), db: Session = Depends(get_db), current_user: User = Depends(get_current_super_admin), ): """ Reset menu configuration for a platform to defaults. Removes all visibility overrides, making all items visible. Super admin only. """ # Verify platform exists platform = platform_service.get_platform_by_id(db, platform_id) menu_service.reset_platform_menu_config(db, frontend_type, platform_id) db.commit() logger.info( f"[MENU_CONFIG] Super admin {current_user.email} reset menu config " f"for platform {platform.code} ({frontend_type.value})" ) 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( db: Session = Depends(get_db), current_user: User = 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" ) return _build_menu_config_response( items, FrontendType.ADMIN, user_id=current_user.id ) @router.put("/user") async def update_user_menu_visibility( update_data: MenuVisibilityUpdateRequest, db: Session = Depends(get_db), current_user: User = 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: User = 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: User = 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"), frontend_type: FrontendType = Query( FrontendType.ADMIN, description="Frontend type (admin or vendor)" ), db: Session = Depends(get_db), current_user: User = Depends(get_current_super_admin), ): """ Show all menu items for a platform. Super admin only. """ # Verify platform exists platform = platform_service.get_platform_by_id(db, platform_id) menu_service.show_all_platform_menu_config(db, frontend_type, platform_id) db.commit() logger.info( f"[MENU_CONFIG] Super admin {current_user.email} enabled all menu items " f"for platform {platform.code} ({frontend_type.value})" ) return {"success": True, "message": "All menu items are now visible"} # ============================================================================= # Menu Rendering (For Sidebar) # ============================================================================= @router.get("/render/admin", response_model=RenderedMenuResponse) async def get_rendered_admin_menu( db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_from_cookie_or_header), ): """ Get the rendered admin menu for the current user. Returns the filtered menu structure based on: - Super admins: user-level config - Platform admins: platform-level config Used by the frontend to render the sidebar. """ if current_user.is_super_admin: # Super admin: use user-level config menu = menu_service.get_menu_for_rendering( db=db, frontend_type=FrontendType.ADMIN, user_id=current_user.id, is_super_admin=True, ) else: # Platform admin: use platform-level config # Get the selected platform from the JWT token platform_id = getattr(current_user, "token_platform_id", None) # Fallback to first platform if no platform in token (shouldn't happen) if platform_id is None and current_user.admin_platforms: platform_id = current_user.admin_platforms[0].id logger.warning( f"[MENU_CONFIG] No platform_id in token for {current_user.email}, " f"falling back to first platform: {platform_id}" ) menu = menu_service.get_menu_for_rendering( db=db, frontend_type=FrontendType.ADMIN, platform_id=platform_id, is_super_admin=False, ) sections = [ MenuSectionResponse( id=section["id"], label=section.get("label"), items=section["items"], ) for section in menu.get("sections", []) ] return RenderedMenuResponse( frontend_type=FrontendType.ADMIN.value, sections=sections, )