feat: add Admin UI for platform module management (Phase 5)

Added a complete Admin UI for managing platform modules:

API endpoints (app/api/v1/admin/modules.py):
- GET /modules - List all available modules
- GET /modules/platforms/{id} - Get platform modules
- PUT /modules/platforms/{id} - Update enabled modules
- POST /modules/platforms/{id}/enable - Enable a module
- POST /modules/platforms/{id}/disable - Disable a module
- POST /modules/platforms/{id}/enable-all - Enable all
- POST /modules/platforms/{id}/disable-optional - Core only

Admin UI:
- New page route: /admin/platforms/{code}/modules
- Template: platform-modules.html
- JavaScript: platform-modules.js
- Link added to platform-detail.html Super Admin section

Features:
- Toggle modules on/off with dependency resolution
- Enable all / Core only bulk actions
- Visual dependency indicators
- Separate sections for core vs optional modules
- Feature list preview per module

Also includes require_menu_access updates to page routes from Phase 2.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 22:08:13 +01:00
parent 9d0dc51de0
commit 7ecd554454
6 changed files with 1061 additions and 81 deletions

View File

@@ -61,6 +61,7 @@ from . import (
media, media,
menu_config, menu_config,
messages, messages,
modules,
monitoring, monitoring,
notifications, notifications,
order_item_exceptions, order_item_exceptions,
@@ -125,6 +126,9 @@ router.include_router(platforms.router, tags=["admin-platforms"])
# Include menu configuration endpoints (super admin only) # Include menu configuration endpoints (super admin only)
router.include_router(menu_config.router, tags=["admin-menu-config"]) router.include_router(menu_config.router, tags=["admin-menu-config"])
# Include module management endpoints (super admin only)
router.include_router(modules.router, tags=["admin-modules"])
# ============================================================================ # ============================================================================
# User Management # User Management

399
app/api/v1/admin/modules.py Normal file
View File

@@ -0,0 +1,399 @@
# app/api/v1/admin/modules.py
"""
Admin API endpoints for Platform Module Management.
Provides module enablement/disablement for platforms:
- GET /modules - List all available modules
- GET /modules/platforms/{platform_id} - Get modules for a platform
- PUT /modules/platforms/{platform_id} - Update enabled modules
- POST /modules/platforms/{platform_id}/enable - Enable a module
- POST /modules/platforms/{platform_id}/disable - Disable a module
All endpoints require super admin access.
"""
import logging
from fastapi import APIRouter, Depends, Path
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.api.deps import get_current_super_admin, get_db
from app.modules.registry import MODULES, get_core_module_codes
from app.modules.service import module_service
from app.services.platform_service import platform_service
from models.database.user import User
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/modules")
# =============================================================================
# Pydantic Schemas
# =============================================================================
class ModuleResponse(BaseModel):
"""Module definition response."""
code: str
name: str
description: str
is_core: bool
is_enabled: bool
requires: list[str] = Field(default_factory=list)
features: list[str] = Field(default_factory=list)
menu_items_admin: list[str] = Field(default_factory=list)
menu_items_vendor: list[str] = Field(default_factory=list)
dependent_modules: list[str] = Field(default_factory=list)
class ModuleListResponse(BaseModel):
"""Response for module list."""
modules: list[ModuleResponse]
total: int
enabled: int
disabled: int
class PlatformModulesResponse(BaseModel):
"""Response for platform module configuration."""
platform_id: int
platform_code: str
platform_name: str
modules: list[ModuleResponse]
total: int
enabled: int
disabled: int
class EnableModulesRequest(BaseModel):
"""Request to set enabled modules."""
module_codes: list[str] = Field(..., description="List of module codes to enable")
class ToggleModuleRequest(BaseModel):
"""Request to enable/disable a single module."""
module_code: str = Field(..., description="Module code to toggle")
# =============================================================================
# Helper Functions
# =============================================================================
def _get_dependent_modules(module_code: str) -> list[str]:
"""Get modules that depend on a given module."""
dependents = []
for code, module in MODULES.items():
if module_code in module.requires:
dependents.append(code)
return dependents
def _build_module_response(
code: str,
is_enabled: bool,
) -> ModuleResponse:
"""Build ModuleResponse from module code."""
from models.database.admin_menu_config import FrontendType
module = MODULES.get(code)
if not module:
raise ValueError(f"Unknown module: {code}")
return ModuleResponse(
code=module.code,
name=module.name,
description=module.description,
is_core=module.is_core,
is_enabled=is_enabled,
requires=module.requires,
features=module.features,
menu_items_admin=module.get_menu_items(FrontendType.ADMIN),
menu_items_vendor=module.get_menu_items(FrontendType.VENDOR),
dependent_modules=_get_dependent_modules(module.code),
)
# =============================================================================
# API Endpoints
# =============================================================================
@router.get("", response_model=ModuleListResponse)
async def list_all_modules(
current_user: User = Depends(get_current_super_admin),
):
"""
List all available modules.
Returns all module definitions with their metadata.
Super admin only.
"""
modules = []
for code in MODULES.keys():
# All modules shown as enabled in the global list
modules.append(_build_module_response(code, is_enabled=True))
# Sort: core first, then alphabetically
modules.sort(key=lambda m: (not m.is_core, m.name))
logger.info(f"[MODULES] Super admin {current_user.email} listed all modules")
return ModuleListResponse(
modules=modules,
total=len(modules),
enabled=len(modules),
disabled=0,
)
@router.get("/platforms/{platform_id}", response_model=PlatformModulesResponse)
async def get_platform_modules(
platform_id: int = Path(..., description="Platform ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Get module configuration for a platform.
Returns all modules with their enablement status for the platform.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Get enabled module codes for this platform
enabled_codes = module_service.get_enabled_module_codes(db, platform_id)
modules = []
for code in MODULES.keys():
is_enabled = code in enabled_codes
modules.append(_build_module_response(code, is_enabled))
# Sort: core first, then alphabetically
modules.sort(key=lambda m: (not m.is_core, m.name))
enabled_count = sum(1 for m in modules if m.is_enabled)
logger.info(
f"[MODULES] Super admin {current_user.email} fetched modules "
f"for platform {platform.code} ({enabled_count}/{len(modules)} enabled)"
)
return PlatformModulesResponse(
platform_id=platform.id,
platform_code=platform.code,
platform_name=platform.name,
modules=modules,
total=len(modules),
enabled=enabled_count,
disabled=len(modules) - enabled_count,
)
@router.put("/platforms/{platform_id}", response_model=PlatformModulesResponse)
async def update_platform_modules(
update_data: EnableModulesRequest,
platform_id: int = Path(..., description="Platform ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Update enabled modules for a platform.
Sets the list of enabled modules. Core modules are automatically included.
Dependencies are automatically resolved.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Update enabled modules
module_service.set_enabled_modules(db, platform_id, update_data.module_codes)
db.commit()
# Get updated module list
enabled_codes = module_service.get_enabled_module_codes(db, platform_id)
modules = []
for code in MODULES.keys():
is_enabled = code in enabled_codes
modules.append(_build_module_response(code, is_enabled))
modules.sort(key=lambda m: (not m.is_core, m.name))
enabled_count = sum(1 for m in modules if m.is_enabled)
logger.info(
f"[MODULES] Super admin {current_user.email} updated modules "
f"for platform {platform.code}: {sorted(update_data.module_codes)}"
)
return PlatformModulesResponse(
platform_id=platform.id,
platform_code=platform.code,
platform_name=platform.name,
modules=modules,
total=len(modules),
enabled=enabled_count,
disabled=len(modules) - enabled_count,
)
@router.post("/platforms/{platform_id}/enable")
async def enable_module(
request: ToggleModuleRequest,
platform_id: int = Path(..., description="Platform ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Enable a single module for a platform.
Also enables required dependencies.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Validate module code
if request.module_code not in MODULES:
from app.exceptions import BadRequestException
raise BadRequestException(f"Unknown module: {request.module_code}")
# Enable module
success = module_service.enable_module(db, platform_id, request.module_code)
if success:
db.commit()
# Check what dependencies were also enabled
module = MODULES[request.module_code]
enabled_deps = module.requires if module.requires else []
logger.info(
f"[MODULES] Super admin {current_user.email} enabled module "
f"'{request.module_code}' for platform {platform.code}"
)
return {
"success": success,
"message": f"Module '{request.module_code}' enabled",
"also_enabled": enabled_deps,
}
@router.post("/platforms/{platform_id}/disable")
async def disable_module(
request: ToggleModuleRequest,
platform_id: int = Path(..., description="Platform ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Disable a single module for a platform.
Core modules cannot be disabled.
Also disables modules that depend on this one.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Validate module code
if request.module_code not in MODULES:
from app.exceptions import BadRequestException
raise BadRequestException(f"Unknown module: {request.module_code}")
# Check if core module
if request.module_code in get_core_module_codes():
from app.exceptions import BadRequestException
raise BadRequestException(f"Cannot disable core module: {request.module_code}")
# Get dependent modules before disabling
dependents = _get_dependent_modules(request.module_code)
# Disable module
success = module_service.disable_module(db, platform_id, request.module_code)
if success:
db.commit()
logger.info(
f"[MODULES] Super admin {current_user.email} disabled module "
f"'{request.module_code}' for platform {platform.code}"
)
return {
"success": success,
"message": f"Module '{request.module_code}' disabled",
"also_disabled": dependents if dependents else [],
}
@router.post("/platforms/{platform_id}/enable-all")
async def enable_all_modules(
platform_id: int = Path(..., description="Platform ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Enable all modules for a platform.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Enable all modules
all_codes = list(MODULES.keys())
module_service.set_enabled_modules(db, platform_id, all_codes)
db.commit()
logger.info(
f"[MODULES] Super admin {current_user.email} enabled all modules "
f"for platform {platform.code}"
)
return {
"success": True,
"message": "All modules enabled",
"enabled_count": len(all_codes),
}
@router.post("/platforms/{platform_id}/disable-optional")
async def disable_optional_modules(
platform_id: int = Path(..., description="Platform ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Disable all optional modules for a platform, keeping only core modules.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Enable only core modules
core_codes = list(get_core_module_codes())
module_service.set_enabled_modules(db, platform_id, core_codes)
db.commit()
logger.info(
f"[MODULES] Super admin {current_user.email} disabled optional modules "
f"for platform {platform.code} (kept {len(core_codes)} core modules)"
)
return {
"success": True,
"message": "Optional modules disabled, core modules kept",
"core_modules": core_codes,
}

View File

@@ -48,11 +48,12 @@ from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import ( from app.api.deps import (
get_current_admin_from_cookie_or_header,
get_current_admin_optional, get_current_admin_optional,
get_db, get_db,
require_menu_access,
) )
from app.core.config import settings from app.core.config import settings
from models.database.admin_menu_config import FrontendType
from models.database.user import User from models.database.user import User
router = APIRouter() router = APIRouter()
@@ -132,7 +133,7 @@ async def admin_select_platform_page(
@router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False) @router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False)
async def admin_dashboard_page( async def admin_dashboard_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("dashboard", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -156,7 +157,7 @@ async def admin_dashboard_page(
@router.get("/companies", response_class=HTMLResponse, include_in_schema=False) @router.get("/companies", response_class=HTMLResponse, include_in_schema=False)
async def admin_companies_list_page( async def admin_companies_list_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -175,7 +176,7 @@ async def admin_companies_list_page(
@router.get("/companies/create", response_class=HTMLResponse, include_in_schema=False) @router.get("/companies/create", response_class=HTMLResponse, include_in_schema=False)
async def admin_company_create_page( async def admin_company_create_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -196,7 +197,7 @@ async def admin_company_create_page(
async def admin_company_detail_page( async def admin_company_detail_page(
request: Request, request: Request,
company_id: int = Path(..., description="Company ID"), company_id: int = Path(..., description="Company ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -218,7 +219,7 @@ async def admin_company_detail_page(
async def admin_company_edit_page( async def admin_company_edit_page(
request: Request, request: Request,
company_id: int = Path(..., description="Company ID"), company_id: int = Path(..., description="Company ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -242,7 +243,7 @@ async def admin_company_edit_page(
@router.get("/vendors", response_class=HTMLResponse, include_in_schema=False) @router.get("/vendors", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendors_list_page( async def admin_vendors_list_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -261,7 +262,7 @@ async def admin_vendors_list_page(
@router.get("/vendors/create", response_class=HTMLResponse, include_in_schema=False) @router.get("/vendors/create", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendor_create_page( async def admin_vendor_create_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -282,7 +283,7 @@ async def admin_vendor_create_page(
async def admin_vendor_detail_page( async def admin_vendor_detail_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -305,7 +306,7 @@ async def admin_vendor_detail_page(
async def admin_vendor_edit_page( async def admin_vendor_edit_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -334,7 +335,7 @@ async def admin_vendor_edit_page(
async def admin_vendor_domains_page( async def admin_vendor_domains_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -359,7 +360,7 @@ async def admin_vendor_domains_page(
@router.get("/vendor-themes", response_class=HTMLResponse, include_in_schema=False) @router.get("/vendor-themes", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendor_themes_page( async def admin_vendor_themes_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendor-themes", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -381,7 +382,7 @@ async def admin_vendor_themes_page(
async def admin_vendor_theme_page( async def admin_vendor_theme_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendor-themes", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -406,18 +407,14 @@ async def admin_vendor_theme_page(
@router.get("/admin-users", response_class=HTMLResponse, include_in_schema=False) @router.get("/admin-users", response_class=HTMLResponse, include_in_schema=False)
async def admin_users_list_page( async def admin_users_list_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
Render admin users management page. Render admin users management page.
Shows list of all admin users (super admins and platform admins). Shows list of all admin users (super admins and platform admins).
Super admin only. Super admin only (menu is in super_admin_only section).
""" """
from fastapi import HTTPException
if not current_user.is_super_admin:
raise HTTPException(status_code=403, detail="Super admin access required")
return templates.TemplateResponse( return templates.TemplateResponse(
"admin/admin-users.html", "admin/admin-users.html",
@@ -431,17 +428,13 @@ async def admin_users_list_page(
@router.get("/admin-users/create", response_class=HTMLResponse, include_in_schema=False) @router.get("/admin-users/create", response_class=HTMLResponse, include_in_schema=False)
async def admin_user_create_page( async def admin_user_create_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
Render admin user creation form. Render admin user creation form.
Super admin only. Super admin only (menu is in super_admin_only section).
""" """
from fastapi import HTTPException
if not current_user.is_super_admin:
raise HTTPException(status_code=403, detail="Super admin access required")
return templates.TemplateResponse( return templates.TemplateResponse(
"admin/user-create.html", "admin/user-create.html",
@@ -458,17 +451,13 @@ async def admin_user_create_page(
async def admin_user_detail_page( async def admin_user_detail_page(
request: Request, request: Request,
user_id: int = Path(..., description="User ID"), user_id: int = Path(..., description="User ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
Render admin user detail view. Render admin user detail view.
Super admin only. Super admin only (menu is in super_admin_only section).
""" """
from fastapi import HTTPException
if not current_user.is_super_admin:
raise HTTPException(status_code=403, detail="Super admin access required")
return templates.TemplateResponse( return templates.TemplateResponse(
"admin/admin-user-detail.html", "admin/admin-user-detail.html",
@@ -486,17 +475,13 @@ async def admin_user_detail_page(
async def admin_user_edit_page( async def admin_user_edit_page(
request: Request, request: Request,
user_id: int = Path(..., description="User ID"), user_id: int = Path(..., description="User ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
Render admin user edit form. Render admin user edit form.
Super admin only. Super admin only (menu is in super_admin_only section).
""" """
from fastapi import HTTPException
if not current_user.is_super_admin:
raise HTTPException(status_code=403, detail="Super admin access required")
return templates.TemplateResponse( return templates.TemplateResponse(
"admin/admin-user-edit.html", "admin/admin-user-edit.html",
@@ -557,7 +542,7 @@ async def admin_user_edit_page_redirect(user_id: int = Path(..., description="Us
@router.get("/customers", response_class=HTMLResponse, include_in_schema=False) @router.get("/customers", response_class=HTMLResponse, include_in_schema=False)
async def admin_customers_page( async def admin_customers_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("customers", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -581,7 +566,7 @@ async def admin_customers_page(
@router.get("/notifications", response_class=HTMLResponse, include_in_schema=False) @router.get("/notifications", response_class=HTMLResponse, include_in_schema=False)
async def admin_notifications_page( async def admin_notifications_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("notifications", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -605,7 +590,7 @@ async def admin_notifications_page(
@router.get("/email-templates", response_class=HTMLResponse, include_in_schema=False) @router.get("/email-templates", response_class=HTMLResponse, include_in_schema=False)
async def admin_email_templates_page( async def admin_email_templates_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("email-templates", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -629,7 +614,7 @@ async def admin_email_templates_page(
@router.get("/messages", response_class=HTMLResponse, include_in_schema=False) @router.get("/messages", response_class=HTMLResponse, include_in_schema=False)
async def admin_messages_page( async def admin_messages_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("messages", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -653,7 +638,7 @@ async def admin_messages_page(
async def admin_conversation_detail_page( async def admin_conversation_detail_page(
request: Request, request: Request,
conversation_id: int = Path(..., description="Conversation ID"), conversation_id: int = Path(..., description="Conversation ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("messages", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -678,7 +663,7 @@ async def admin_conversation_detail_page(
@router.get("/inventory", response_class=HTMLResponse, include_in_schema=False) @router.get("/inventory", response_class=HTMLResponse, include_in_schema=False)
async def admin_inventory_page( async def admin_inventory_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("inventory", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -702,7 +687,7 @@ async def admin_inventory_page(
@router.get("/orders", response_class=HTMLResponse, include_in_schema=False) @router.get("/orders", response_class=HTMLResponse, include_in_schema=False)
async def admin_orders_page( async def admin_orders_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("orders", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -726,7 +711,7 @@ async def admin_orders_page(
@router.get("/imports", response_class=HTMLResponse, include_in_schema=False) @router.get("/imports", response_class=HTMLResponse, include_in_schema=False)
async def admin_imports_page( async def admin_imports_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("imports", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -745,7 +730,7 @@ async def admin_imports_page(
@router.get("/background-tasks", response_class=HTMLResponse, include_in_schema=False) @router.get("/background-tasks", response_class=HTMLResponse, include_in_schema=False)
async def admin_background_tasks_page( async def admin_background_tasks_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("background-tasks", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -765,7 +750,7 @@ async def admin_background_tasks_page(
@router.get("/marketplace", response_class=HTMLResponse, include_in_schema=False) @router.get("/marketplace", response_class=HTMLResponse, include_in_schema=False)
async def admin_marketplace_page( async def admin_marketplace_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -791,7 +776,7 @@ async def admin_marketplace_page(
) )
async def admin_marketplace_letzshop_page( async def admin_marketplace_letzshop_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -814,7 +799,7 @@ async def admin_marketplace_letzshop_page(
async def admin_letzshop_order_detail_page( async def admin_letzshop_order_detail_page(
request: Request, request: Request,
order_id: int = Path(..., description="Letzshop order ID"), order_id: int = Path(..., description="Letzshop order ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -840,7 +825,7 @@ async def admin_letzshop_order_detail_page(
async def admin_letzshop_product_detail_page( async def admin_letzshop_product_detail_page(
request: Request, request: Request,
product_id: int = Path(..., description="Marketplace Product ID"), product_id: int = Path(..., description="Marketplace Product ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -870,7 +855,7 @@ async def admin_letzshop_product_detail_page(
) )
async def admin_letzshop_vendor_directory_page( async def admin_letzshop_vendor_directory_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -900,7 +885,7 @@ async def admin_letzshop_vendor_directory_page(
) )
async def admin_marketplace_products_page( async def admin_marketplace_products_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -924,7 +909,7 @@ async def admin_marketplace_products_page(
async def admin_marketplace_product_detail_page( async def admin_marketplace_product_detail_page(
request: Request, request: Request,
product_id: int = Path(..., description="Marketplace Product ID"), product_id: int = Path(..., description="Marketplace Product ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -945,7 +930,7 @@ async def admin_marketplace_product_detail_page(
@router.get("/vendor-products", response_class=HTMLResponse, include_in_schema=False) @router.get("/vendor-products", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendor_products_page( async def admin_vendor_products_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -964,7 +949,7 @@ async def admin_vendor_products_page(
@router.get("/vendor-products/create", response_class=HTMLResponse, include_in_schema=False) @router.get("/vendor-products/create", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendor_product_create_page( async def admin_vendor_product_create_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -988,7 +973,7 @@ async def admin_vendor_product_create_page(
async def admin_vendor_product_detail_page( async def admin_vendor_product_detail_page(
request: Request, request: Request,
product_id: int = Path(..., description="Vendor Product ID"), product_id: int = Path(..., description="Vendor Product ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1013,7 +998,7 @@ async def admin_vendor_product_detail_page(
async def admin_vendor_product_edit_page( async def admin_vendor_product_edit_page(
request: Request, request: Request,
product_id: int = Path(..., description="Vendor Product ID"), product_id: int = Path(..., description="Vendor Product ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1038,7 +1023,7 @@ async def admin_vendor_product_edit_page(
@router.get("/subscription-tiers", response_class=HTMLResponse, include_in_schema=False) @router.get("/subscription-tiers", response_class=HTMLResponse, include_in_schema=False)
async def admin_subscription_tiers_page( async def admin_subscription_tiers_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("subscription-tiers", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1057,7 +1042,7 @@ async def admin_subscription_tiers_page(
@router.get("/subscriptions", response_class=HTMLResponse, include_in_schema=False) @router.get("/subscriptions", response_class=HTMLResponse, include_in_schema=False)
async def admin_subscriptions_page( async def admin_subscriptions_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("subscriptions", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1076,7 +1061,7 @@ async def admin_subscriptions_page(
@router.get("/billing-history", response_class=HTMLResponse, include_in_schema=False) @router.get("/billing-history", response_class=HTMLResponse, include_in_schema=False)
async def admin_billing_history_page( async def admin_billing_history_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("billing-history", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1100,7 +1085,7 @@ async def admin_billing_history_page(
@router.get("/settings", response_class=HTMLResponse, include_in_schema=False) @router.get("/settings", response_class=HTMLResponse, include_in_schema=False)
async def admin_settings_page( async def admin_settings_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("settings", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1116,10 +1101,33 @@ async def admin_settings_page(
) )
@router.get("/my-menu", response_class=HTMLResponse, include_in_schema=False)
async def admin_my_menu_config(
request: Request,
current_user: User = Depends(require_menu_access("my-menu", FrontendType.ADMIN)),
db: Session = Depends(get_db),
):
"""
Render personal menu configuration page for super admins.
Allows super admins to customize their own sidebar menu.
"""
# Only super admins can configure their own menu
if not current_user.is_super_admin:
return RedirectResponse(url="/admin/settings", status_code=302)
return templates.TemplateResponse(
"admin/my-menu-config.html",
{
"request": request,
"user": current_user,
},
)
@router.get("/logs", response_class=HTMLResponse, include_in_schema=False) @router.get("/logs", response_class=HTMLResponse, include_in_schema=False)
async def admin_logs_page( async def admin_logs_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("logs", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1143,7 +1151,7 @@ async def admin_logs_page(
@router.get("/platforms", response_class=HTMLResponse, include_in_schema=False) @router.get("/platforms", response_class=HTMLResponse, include_in_schema=False)
async def admin_platforms_list( async def admin_platforms_list(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1165,7 +1173,7 @@ async def admin_platforms_list(
async def admin_platform_detail( async def admin_platform_detail(
request: Request, request: Request,
platform_code: str = Path(..., description="Platform code (oms, loyalty, etc.)"), platform_code: str = Path(..., description="Platform code (oms, loyalty, etc.)"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1190,7 +1198,7 @@ async def admin_platform_detail(
async def admin_platform_edit( async def admin_platform_edit(
request: Request, request: Request,
platform_code: str = Path(..., description="Platform code"), platform_code: str = Path(..., description="Platform code"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1207,6 +1215,66 @@ async def admin_platform_edit(
) )
@router.get(
"/platforms/{platform_code}/menu-config",
response_class=HTMLResponse,
include_in_schema=False,
)
async def admin_platform_menu_config(
request: Request,
platform_code: str = Path(..., description="Platform code"),
current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)),
db: Session = Depends(get_db),
):
"""
Render platform menu configuration page.
Super admin only - allows configuring which menu items are visible
for the platform's admin and vendor frontends.
"""
# Only super admins can access menu configuration
if not current_user.is_super_admin:
return RedirectResponse(url=f"/admin/platforms/{platform_code}", status_code=302)
return templates.TemplateResponse(
"admin/platform-menu-config.html",
{
"request": request,
"user": current_user,
"platform_code": platform_code,
},
)
@router.get(
"/platforms/{platform_code}/modules",
response_class=HTMLResponse,
include_in_schema=False,
)
async def admin_platform_modules(
request: Request,
platform_code: str = Path(..., description="Platform code"),
current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)),
db: Session = Depends(get_db),
):
"""
Render platform module configuration page.
Super admin only - allows enabling/disabling feature modules
for the platform.
"""
# Only super admins can access module configuration
if not current_user.is_super_admin:
return RedirectResponse(url=f"/admin/platforms/{platform_code}", status_code=302)
return templates.TemplateResponse(
"admin/platform-modules.html",
{
"request": request,
"user": current_user,
"platform_code": platform_code,
},
)
# ============================================================================ # ============================================================================
# CONTENT MANAGEMENT SYSTEM (CMS) ROUTES # CONTENT MANAGEMENT SYSTEM (CMS) ROUTES
# ============================================================================ # ============================================================================
@@ -1215,7 +1283,7 @@ async def admin_platform_edit(
@router.get("/platform-homepage", include_in_schema=False) @router.get("/platform-homepage", include_in_schema=False)
async def admin_platform_homepage_manager( async def admin_platform_homepage_manager(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1225,14 +1293,13 @@ async def admin_platform_homepage_manager(
- /admin/platforms → Select platform → Homepage button - /admin/platforms → Select platform → Homepage button
- Or directly: /admin/content-pages?platform_code={code}&slug=home - Or directly: /admin/content-pages?platform_code={code}&slug=home
""" """
from starlette.responses import RedirectResponse
return RedirectResponse(url="/admin/platforms", status_code=302) return RedirectResponse(url="/admin/platforms", status_code=302)
@router.get("/content-pages", response_class=HTMLResponse, include_in_schema=False) @router.get("/content-pages", response_class=HTMLResponse, include_in_schema=False)
async def admin_content_pages_list( async def admin_content_pages_list(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("content-pages", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1253,7 +1320,7 @@ async def admin_content_pages_list(
) )
async def admin_content_page_create( async def admin_content_page_create(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("content-pages", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1278,7 +1345,7 @@ async def admin_content_page_create(
async def admin_content_page_edit( async def admin_content_page_edit(
request: Request, request: Request,
page_id: int = Path(..., description="Content page ID"), page_id: int = Path(..., description="Content page ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("content-pages", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1303,7 +1370,7 @@ async def admin_content_page_edit(
@router.get("/components", response_class=HTMLResponse, include_in_schema=False) @router.get("/components", response_class=HTMLResponse, include_in_schema=False)
async def admin_components_page( async def admin_components_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("components", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1322,7 +1389,7 @@ async def admin_components_page(
@router.get("/icons", response_class=HTMLResponse, include_in_schema=False) @router.get("/icons", response_class=HTMLResponse, include_in_schema=False)
async def admin_icons_page( async def admin_icons_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("icons", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1341,7 +1408,7 @@ async def admin_icons_page(
@router.get("/testing", response_class=HTMLResponse, include_in_schema=False) @router.get("/testing", response_class=HTMLResponse, include_in_schema=False)
async def admin_testing_dashboard( async def admin_testing_dashboard(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1360,7 +1427,7 @@ async def admin_testing_dashboard(
@router.get("/testing-hub", response_class=HTMLResponse, include_in_schema=False) @router.get("/testing-hub", response_class=HTMLResponse, include_in_schema=False)
async def admin_testing_hub( async def admin_testing_hub(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1379,7 +1446,7 @@ async def admin_testing_hub(
@router.get("/test/auth-flow", response_class=HTMLResponse, include_in_schema=False) @router.get("/test/auth-flow", response_class=HTMLResponse, include_in_schema=False)
async def admin_test_auth_flow( async def admin_test_auth_flow(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1402,7 +1469,7 @@ async def admin_test_auth_flow(
) )
async def admin_test_vendors_users_migration( async def admin_test_vendors_users_migration(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1426,7 +1493,7 @@ async def admin_test_vendors_users_migration(
@router.get("/code-quality", response_class=HTMLResponse, include_in_schema=False) @router.get("/code-quality", response_class=HTMLResponse, include_in_schema=False)
async def admin_code_quality_dashboard( async def admin_code_quality_dashboard(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("code-quality", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1447,7 +1514,7 @@ async def admin_code_quality_dashboard(
) )
async def admin_code_quality_violations( async def admin_code_quality_violations(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("code-quality", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1471,7 +1538,7 @@ async def admin_code_quality_violations(
async def admin_code_quality_violation_detail( async def admin_code_quality_violation_detail(
request: Request, request: Request,
violation_id: int = Path(..., description="Violation ID"), violation_id: int = Path(..., description="Violation ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("code-quality", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1496,7 +1563,7 @@ async def admin_code_quality_violation_detail(
@router.get("/platform-health", response_class=HTMLResponse, include_in_schema=False) @router.get("/platform-health", response_class=HTMLResponse, include_in_schema=False)
async def admin_platform_health( async def admin_platform_health(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("platform-health", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
@@ -1520,7 +1587,7 @@ async def admin_platform_health(
@router.get("/features", response_class=HTMLResponse, include_in_schema=False) @router.get("/features", response_class=HTMLResponse, include_in_schema=False)
async def admin_features_page( async def admin_features_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header), current_user: User = Depends(require_menu_access("subscription-tiers", FrontendType.ADMIN)),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """

View File

@@ -90,6 +90,37 @@
</div> </div>
</div> </div>
<!-- Super Admin Actions (Menu Configuration) -->
<div x-show="isSuperAdmin" class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span class="inline-flex items-center">
Super Admin
<span class="ml-2 px-2 py-0.5 text-xs font-medium rounded bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">Admin Only</span>
</span>
</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- Module Configuration -->
<a :href="`/admin/platforms/${platformCode}/modules`"
class="flex items-center p-4 bg-green-50 dark:bg-green-900/20 rounded-lg hover:bg-green-100 dark:hover:bg-green-900/40 transition-colors">
<span x-html="$icon('puzzle', 'w-8 h-8 text-green-600 dark:text-green-400')"></span>
<div class="ml-3">
<p class="font-semibold text-gray-900 dark:text-white">Module Configuration</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Enable/disable features</p>
</div>
</a>
<!-- Menu Configuration -->
<a :href="`/admin/platforms/${platformCode}/menu-config`"
class="flex items-center p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg hover:bg-amber-100 dark:hover:bg-amber-900/40 transition-colors">
<span x-html="$icon('view-grid', 'w-8 h-8 text-amber-600 dark:text-amber-400')"></span>
<div class="ml-3">
<p class="font-semibold text-gray-900 dark:text-white">Menu Configuration</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Admin & vendor menus</p>
</div>
</a>
</div>
</div>
<!-- Stats Grid --> <!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6"> <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<!-- Vendors --> <!-- Vendors -->

View File

@@ -0,0 +1,252 @@
{# app/templates/admin/platform-modules.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
{% from 'shared/macros/headers.html' import page_header %}
{% block title %}Module Configuration{% endblock %}
{% block alpine_data %}adminPlatformModules('{{ platform_code }}'){% endblock %}
{% block content %}
{{ page_header('Module Configuration', back_url='/admin/platforms/' + platform_code) }}
{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }}
{{ error_state('Error', show_condition='error') }}
<!-- Platform Info -->
<div class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="platform?.name || 'Loading...'"></h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
Enable or disable feature modules for this platform. Core modules cannot be disabled.
</p>
</div>
<span class="px-3 py-1 text-sm font-medium rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200" x-text="platform?.code?.toUpperCase()"></span>
</div>
</div>
<!-- Stats Cards -->
<div class="grid gap-4 mb-6 md:grid-cols-4">
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('puzzle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Total Modules</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="moduleConfig?.total || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Enabled</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="moduleConfig?.enabled || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-gray-500 bg-gray-100 rounded-full dark:text-gray-100 dark:bg-gray-600">
<span x-html="$icon('x-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Disabled</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="moduleConfig?.disabled || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('shield', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Core Modules</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="coreModulesCount"></p>
</div>
</div>
</div>
<!-- Actions -->
<div class="mb-4 flex items-center justify-between">
<p class="text-sm text-gray-500 dark:text-gray-400">
Toggle modules on/off. Dependencies are resolved automatically.
</p>
<div class="flex gap-2">
<button
@click="enableAll()"
:disabled="saving"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
<span x-html="$icon('check-circle', 'w-4 h-4 mr-2')"></span>
Enable All
</button>
<button
@click="disableOptional()"
:disabled="saving"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
<span x-html="$icon('x-circle', 'w-4 h-4 mr-2')"></span>
Core Only
</button>
</div>
</div>
<!-- Loading State -->
<div x-show="loading" class="flex items-center justify-center py-12 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<span x-html="$icon('refresh', 'w-8 h-8 animate-spin text-purple-600')"></span>
<span class="ml-3 text-gray-500 dark:text-gray-400">Loading module configuration...</span>
</div>
<!-- Module Groups -->
<div x-show="!loading" class="space-y-6">
<!-- Core Modules -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<div class="px-4 py-3 bg-purple-50 dark:bg-purple-900/20 border-b border-purple-200 dark:border-purple-800">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span x-html="$icon('shield', 'w-5 h-5 text-purple-600 dark:text-purple-400 mr-2')"></span>
<h3 class="text-sm font-semibold text-purple-800 dark:text-purple-200">Core Modules</h3>
<span class="ml-2 px-2 py-0.5 text-xs font-medium rounded bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
Always Enabled
</span>
</div>
<span class="text-xs text-purple-600 dark:text-purple-400" x-text="`${coreModulesCount} modules`"></span>
</div>
</div>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<template x-for="module in coreModules" :key="module.code">
<div class="flex items-center justify-between px-4 py-4 hover:bg-gray-50 dark:hover:bg-gray-700/30">
<div class="flex items-center flex-1">
<div class="p-2 mr-3 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
<span x-html="$icon(getModuleIcon(module.code), 'w-5 h-5 text-purple-600 dark:text-purple-400')"></span>
</div>
<div class="flex-1">
<p class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="module.name"></p>
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="module.description"></p>
<!-- Features -->
<div x-show="module.features?.length > 0" class="mt-1 flex flex-wrap gap-1">
<template x-for="feature in module.features.slice(0, 3)" :key="feature">
<span class="px-1.5 py-0.5 text-xs rounded bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400" x-text="feature"></span>
</template>
<span x-show="module.features?.length > 3" class="text-xs text-gray-400" x-text="`+${module.features.length - 3} more`"></span>
</div>
</div>
</div>
<div class="flex items-center gap-3 ml-4">
<span class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Enabled
</span>
</div>
</div>
</template>
</div>
</div>
<!-- Optional Modules -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span x-html="$icon('puzzle', 'w-5 h-5 text-gray-600 dark:text-gray-400 mr-2')"></span>
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200">Optional Modules</h3>
</div>
<span class="text-xs text-gray-500 dark:text-gray-400" x-text="`${enabledOptionalCount}/${optionalModules.length} enabled`"></span>
</div>
</div>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<template x-for="module in optionalModules" :key="module.code">
<div class="flex items-center justify-between px-4 py-4 hover:bg-gray-50 dark:hover:bg-gray-700/30">
<div class="flex items-center flex-1">
<div class="p-2 mr-3 rounded-lg"
:class="module.is_enabled ? 'bg-green-100 dark:bg-green-900/30' : 'bg-gray-100 dark:bg-gray-700'">
<span x-html="$icon(getModuleIcon(module.code), 'w-5 h-5')"
:class="module.is_enabled ? 'text-green-600 dark:text-green-400' : 'text-gray-400'"></span>
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<p class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="module.name"></p>
<!-- Dependencies Badge -->
<template x-if="module.requires?.length > 0">
<span class="px-1.5 py-0.5 text-xs rounded bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
:title="`Requires: ${module.requires.join(', ')}`">
<span x-html="$icon('link', 'w-3 h-3 inline')"></span>
<span x-text="module.requires.length"></span>
</span>
</template>
<!-- Dependents Badge -->
<template x-if="module.dependent_modules?.length > 0">
<span class="px-1.5 py-0.5 text-xs rounded bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
:title="`Required by: ${module.dependent_modules.join(', ')}`">
<span x-html="$icon('users', 'w-3 h-3 inline')"></span>
<span x-text="module.dependent_modules.length"></span>
</span>
</template>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="module.description"></p>
<!-- Dependencies Info -->
<div x-show="module.requires?.length > 0" class="mt-1">
<span class="text-xs text-amber-600 dark:text-amber-400">
Requires: <span x-text="module.requires.join(', ')"></span>
</span>
</div>
<!-- Features -->
<div x-show="module.features?.length > 0" class="mt-1 flex flex-wrap gap-1">
<template x-for="feature in module.features.slice(0, 3)" :key="feature">
<span class="px-1.5 py-0.5 text-xs rounded bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400" x-text="feature"></span>
</template>
<span x-show="module.features?.length > 3" class="text-xs text-gray-400" x-text="`+${module.features.length - 3} more`"></span>
</div>
</div>
</div>
<div class="flex items-center gap-3 ml-4">
<!-- Status Badge -->
<span x-show="module.is_enabled"
class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Enabled
</span>
<span x-show="!module.is_enabled"
class="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
Disabled
</span>
<!-- Toggle Switch -->
<button
@click="toggleModule(module)"
:disabled="saving"
:class="{
'bg-purple-600': module.is_enabled,
'bg-gray-200 dark:bg-gray-600': !module.is_enabled
}"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
role="switch"
:aria-checked="module.is_enabled"
>
<span
:class="{
'translate-x-5': module.is_enabled,
'translate-x-0': !module.is_enabled
}"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
></span>
</button>
</div>
</div>
</template>
</div>
</div>
<!-- Empty State -->
<div x-show="moduleConfig?.modules?.length === 0" class="bg-white rounded-lg shadow-xs dark:bg-gray-800 p-8 text-center">
<span x-html="$icon('puzzle', 'w-12 h-12 mx-auto text-gray-400')"></span>
<p class="mt-4 text-gray-500 dark:text-gray-400">No modules available.</p>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/platform-modules.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,227 @@
// static/admin/js/platform-modules.js
// Platform module configuration management
const moduleConfigLog = window.LogConfig?.loggers?.moduleConfig || window.LogConfig?.createLogger?.('moduleConfig') || console;
function adminPlatformModules(platformCode) {
// Get base data with safety check for standalone usage
const baseData = typeof data === 'function' ? data() : {};
return {
// Inherit base layout functionality from init-alpine.js
...baseData,
// Page-specific state
currentPage: 'platforms',
platformCode: platformCode,
loading: true,
error: null,
successMessage: null,
saving: false,
// Data
platform: null,
moduleConfig: null,
// Computed properties
get coreModules() {
if (!this.moduleConfig?.modules) return [];
return this.moduleConfig.modules.filter(m => m.is_core);
},
get optionalModules() {
if (!this.moduleConfig?.modules) return [];
return this.moduleConfig.modules.filter(m => !m.is_core);
},
get coreModulesCount() {
return this.coreModules.length;
},
get enabledOptionalCount() {
return this.optionalModules.filter(m => m.is_enabled).length;
},
// Module icons mapping
getModuleIcon(moduleCode) {
const icons = {
'core': 'home',
'platform-admin': 'building-office',
'billing': 'credit-card',
'inventory': 'archive-box',
'orders': 'shopping-cart',
'marketplace': 'shopping-bag',
'customers': 'users',
'cms': 'document-text',
'analytics': 'chart-bar',
'messaging': 'chat-bubble-left-right',
'dev-tools': 'code-bracket',
'monitoring': 'chart-bar-square'
};
return icons[moduleCode] || 'puzzle';
},
async init() {
moduleConfigLog.info('=== PLATFORM MODULES PAGE INITIALIZING ===');
moduleConfigLog.info('Platform code:', this.platformCode);
try {
await this.loadPlatform();
await this.loadModuleConfig();
moduleConfigLog.info('=== PLATFORM MODULES PAGE INITIALIZED ===');
} catch (error) {
moduleConfigLog.error('Failed to initialize modules page:', error);
this.error = 'Failed to load page data. Please refresh.';
}
},
async refresh() {
this.error = null;
this.successMessage = null;
await this.loadModuleConfig();
},
async loadPlatform() {
try {
this.platform = await apiClient.get(`/admin/platforms/${this.platformCode}`);
moduleConfigLog.info('Loaded platform:', this.platform?.name);
} catch (error) {
moduleConfigLog.error('Failed to load platform:', error);
throw error;
}
},
async loadModuleConfig() {
this.loading = true;
this.error = null;
try {
const platformId = this.platform?.id;
if (!platformId) {
throw new Error('Platform not loaded');
}
this.moduleConfig = await apiClient.get(`/admin/modules/platforms/${platformId}`);
moduleConfigLog.info('Loaded module config:', {
total: this.moduleConfig?.total,
enabled: this.moduleConfig?.enabled,
disabled: this.moduleConfig?.disabled
});
} catch (error) {
moduleConfigLog.error('Failed to load module config:', error);
this.error = error.message || 'Failed to load module configuration';
} finally {
this.loading = false;
}
},
async toggleModule(module) {
if (module.is_core) {
moduleConfigLog.warn('Cannot toggle core module:', module.code);
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
const action = module.is_enabled ? 'disable' : 'enable';
try {
const platformId = this.platform?.id;
const endpoint = `/admin/modules/platforms/${platformId}/${action}`;
const result = await apiClient.post(endpoint, {
module_code: module.code
});
moduleConfigLog.info(`${action}d module:`, module.code, result);
// Show success message
if (result.also_enabled?.length > 0) {
this.successMessage = `Module '${module.name}' enabled. Also enabled dependencies: ${result.also_enabled.join(', ')}`;
} else if (result.also_disabled?.length > 0) {
this.successMessage = `Module '${module.name}' disabled. Also disabled dependents: ${result.also_disabled.join(', ')}`;
} else {
this.successMessage = `Module '${module.name}' ${action}d successfully`;
}
// Reload module config to get updated state
await this.loadModuleConfig();
// Clear success message after delay
setTimeout(() => {
this.successMessage = null;
}, 5000);
} catch (error) {
moduleConfigLog.error(`Failed to ${action} module:`, error);
this.error = error.message || `Failed to ${action} module`;
} finally {
this.saving = false;
}
},
async enableAll() {
if (!confirm('This will enable all modules. Continue?')) {
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
try {
const platformId = this.platform?.id;
const result = await apiClient.post(`/admin/modules/platforms/${platformId}/enable-all`);
moduleConfigLog.info('Enabled all modules:', result);
this.successMessage = `All ${result.enabled_count} modules enabled`;
// Reload module config
await this.loadModuleConfig();
setTimeout(() => {
this.successMessage = null;
}, 5000);
} catch (error) {
moduleConfigLog.error('Failed to enable all modules:', error);
this.error = error.message || 'Failed to enable all modules';
} finally {
this.saving = false;
}
},
async disableOptional() {
if (!confirm('This will disable all optional modules, keeping only core modules. Continue?')) {
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
try {
const platformId = this.platform?.id;
const result = await apiClient.post(`/admin/modules/platforms/${platformId}/disable-optional`);
moduleConfigLog.info('Disabled optional modules:', result);
this.successMessage = `Optional modules disabled. Core modules kept: ${result.core_modules.join(', ')}`;
// Reload module config
await this.loadModuleConfig();
setTimeout(() => {
this.successMessage = null;
}, 5000);
} catch (error) {
moduleConfigLog.error('Failed to disable optional modules:', error);
this.error = error.message || 'Failed to disable optional modules';
} finally {
this.saving = false;
}
}
};
}