diff --git a/app/api/v1/admin/__init__.py b/app/api/v1/admin/__init__.py index feaaaf7c..9fa77be9 100644 --- a/app/api/v1/admin/__init__.py +++ b/app/api/v1/admin/__init__.py @@ -47,6 +47,7 @@ from . import ( order_item_exceptions, orders, platform_health, + platforms, products, settings, subscriptions, @@ -91,6 +92,9 @@ router.include_router( content_pages.router, prefix="/content-pages", tags=["admin-content-pages"] ) +# Include platforms management endpoints (multi-platform CMS) +router.include_router(platforms.router, tags=["admin-platforms"]) + # ============================================================================ # User Management diff --git a/app/api/v1/admin/content_pages.py b/app/api/v1/admin/content_pages.py index 1dbfeab4..f7fbd057 100644 --- a/app/api/v1/admin/content_pages.py +++ b/app/api/v1/admin/content_pages.py @@ -82,6 +82,9 @@ class ContentPageResponse(BaseModel): """Schema for content page response.""" id: int + platform_id: int | None = None + platform_code: str | None = None + platform_name: str | None = None vendor_id: int | None vendor_name: str | None slug: str @@ -97,8 +100,11 @@ class ContentPageResponse(BaseModel): show_in_footer: bool show_in_header: bool show_in_legal: bool - is_platform_default: bool - is_vendor_override: bool + is_platform_page: bool = False + is_platform_default: bool = False # Deprecated: use is_platform_page + is_vendor_default: bool = False + is_vendor_override: bool = False + page_tier: str | None = None created_at: str updated_at: str created_by: int | None diff --git a/app/api/v1/admin/platforms.py b/app/api/v1/admin/platforms.py new file mode 100644 index 00000000..b0491265 --- /dev/null +++ b/app/api/v1/admin/platforms.py @@ -0,0 +1,391 @@ +# app/api/v1/admin/platforms.py +""" +Admin API endpoints for Platform management (Multi-Platform CMS). + +Provides CRUD operations for platforms: +- GET /platforms - List all platforms +- GET /platforms/{code} - Get platform details +- PUT /platforms/{code} - Update platform settings +- GET /platforms/{code}/stats - Get platform statistics + +Platforms are business offerings (OMS, Loyalty, Site Builder) with their own: +- Marketing pages (homepage, pricing, features) +- Vendor defaults (about, terms, privacy) +- Configuration and branding +""" + +import logging +from typing import Any + +from fastapi import APIRouter, Depends, HTTPException, Path, Query +from pydantic import BaseModel, Field +from sqlalchemy import func +from sqlalchemy.orm import Session + +from app.api.deps import get_current_admin_from_cookie_or_header, get_db +from models.database.content_page import ContentPage +from models.database.platform import Platform +from models.database.user import User +from models.database.vendor_platform import VendorPlatform + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/platforms") + + +# ============================================================================= +# Pydantic Schemas +# ============================================================================= + + +class PlatformResponse(BaseModel): + """Platform response schema.""" + + id: int + code: str + name: str + description: str | None = None + domain: str | None = None + path_prefix: str | None = None + logo: str | None = None + logo_dark: str | None = None + favicon: str | None = None + theme_config: dict[str, Any] = Field(default_factory=dict) + default_language: str = "fr" + supported_languages: list[str] = Field(default_factory=lambda: ["fr", "de", "en"]) + is_active: bool = True + is_public: bool = True + settings: dict[str, Any] = Field(default_factory=dict) + created_at: str + updated_at: str + + # Computed fields (added by endpoint) + vendor_count: int = 0 + platform_pages_count: int = 0 + vendor_defaults_count: int = 0 + + class Config: + from_attributes = True + + +class PlatformListResponse(BaseModel): + """Response for platform list.""" + + platforms: list[PlatformResponse] + total: int + + +class PlatformUpdateRequest(BaseModel): + """Request schema for updating a platform.""" + + name: str | None = None + description: str | None = None + domain: str | None = None + path_prefix: str | None = None + logo: str | None = None + logo_dark: str | None = None + favicon: str | None = None + theme_config: dict[str, Any] | None = None + default_language: str | None = None + supported_languages: list[str] | None = None + is_active: bool | None = None + is_public: bool | None = None + settings: dict[str, Any] | None = None + + +class PlatformStatsResponse(BaseModel): + """Platform statistics response.""" + + platform_id: int + platform_code: str + platform_name: str + vendor_count: int + platform_pages_count: int + vendor_defaults_count: int + vendor_overrides_count: int + published_pages_count: int + draft_pages_count: int + + +# ============================================================================= +# API Endpoints +# ============================================================================= + + +@router.get("", response_model=PlatformListResponse) +async def list_platforms( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_admin_from_cookie_or_header), + include_inactive: bool = Query(False, description="Include inactive platforms"), +): + """ + List all platforms with their statistics. + + Returns all platforms (OMS, Loyalty, etc.) with vendor counts and page counts. + """ + query = db.query(Platform) + + if not include_inactive: + query = query.filter(Platform.is_active == True) + + platforms = query.order_by(Platform.id).all() + + # Build response with computed fields + result = [] + for platform in platforms: + # Count vendors on this platform + vendor_count = ( + db.query(func.count(VendorPlatform.vendor_id)) + .filter(VendorPlatform.platform_id == platform.id) + .scalar() + or 0 + ) + + # Count platform marketing pages + platform_pages_count = ( + db.query(func.count(ContentPage.id)) + .filter( + ContentPage.platform_id == platform.id, + ContentPage.vendor_id == None, + ContentPage.is_platform_page == True, + ) + .scalar() + or 0 + ) + + # Count vendor default pages + vendor_defaults_count = ( + db.query(func.count(ContentPage.id)) + .filter( + ContentPage.platform_id == platform.id, + ContentPage.vendor_id == None, + ContentPage.is_platform_page == False, + ) + .scalar() + or 0 + ) + + platform_data = PlatformResponse( + id=platform.id, + code=platform.code, + name=platform.name, + description=platform.description, + domain=platform.domain, + path_prefix=platform.path_prefix, + logo=platform.logo, + logo_dark=platform.logo_dark, + favicon=platform.favicon, + theme_config=platform.theme_config or {}, + default_language=platform.default_language, + supported_languages=platform.supported_languages or ["fr", "de", "en"], + is_active=platform.is_active, + is_public=platform.is_public, + settings=platform.settings or {}, + created_at=platform.created_at.isoformat(), + updated_at=platform.updated_at.isoformat(), + vendor_count=vendor_count, + platform_pages_count=platform_pages_count, + vendor_defaults_count=vendor_defaults_count, + ) + result.append(platform_data) + + logger.info(f"[PLATFORMS] Listed {len(result)} platforms") + + return PlatformListResponse(platforms=result, total=len(result)) + + +@router.get("/{code}", response_model=PlatformResponse) +async def get_platform( + code: str = Path(..., description="Platform code (oms, loyalty, etc.)"), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_admin_from_cookie_or_header), +): + """ + Get platform details by code. + + Returns full platform configuration including statistics. + """ + platform = db.query(Platform).filter(Platform.code == code).first() + + if not platform: + raise HTTPException(status_code=404, detail=f"Platform not found: {code}") + + # Count vendors on this platform + vendor_count = ( + db.query(func.count(VendorPlatform.vendor_id)) + .filter(VendorPlatform.platform_id == platform.id) + .scalar() + or 0 + ) + + # Count platform marketing pages + platform_pages_count = ( + db.query(func.count(ContentPage.id)) + .filter( + ContentPage.platform_id == platform.id, + ContentPage.vendor_id == None, + ContentPage.is_platform_page == True, + ) + .scalar() + or 0 + ) + + # Count vendor default pages + vendor_defaults_count = ( + db.query(func.count(ContentPage.id)) + .filter( + ContentPage.platform_id == platform.id, + ContentPage.vendor_id == None, + ContentPage.is_platform_page == False, + ) + .scalar() + or 0 + ) + + return PlatformResponse( + id=platform.id, + code=platform.code, + name=platform.name, + description=platform.description, + domain=platform.domain, + path_prefix=platform.path_prefix, + logo=platform.logo, + logo_dark=platform.logo_dark, + favicon=platform.favicon, + theme_config=platform.theme_config or {}, + default_language=platform.default_language, + supported_languages=platform.supported_languages or ["fr", "de", "en"], + is_active=platform.is_active, + is_public=platform.is_public, + settings=platform.settings or {}, + created_at=platform.created_at.isoformat(), + updated_at=platform.updated_at.isoformat(), + vendor_count=vendor_count, + platform_pages_count=platform_pages_count, + vendor_defaults_count=vendor_defaults_count, + ) + + +@router.put("/{code}", response_model=PlatformResponse) +async def update_platform( + update_data: PlatformUpdateRequest, + code: str = Path(..., description="Platform code"), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_admin_from_cookie_or_header), +): + """ + Update platform settings. + + Allows updating name, description, branding, and configuration. + """ + platform = db.query(Platform).filter(Platform.code == code).first() + + if not platform: + raise HTTPException(status_code=404, detail=f"Platform not found: {code}") + + # Update fields if provided + update_dict = update_data.model_dump(exclude_unset=True) + for field, value in update_dict.items(): + if hasattr(platform, field): + setattr(platform, field, value) + + db.commit() + db.refresh(platform) + + logger.info(f"[PLATFORMS] Updated platform: {code}") + + # Return updated platform with stats + return await get_platform(code=code, db=db, current_user=current_user) + + +@router.get("/{code}/stats", response_model=PlatformStatsResponse) +async def get_platform_stats( + code: str = Path(..., description="Platform code"), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_admin_from_cookie_or_header), +): + """ + Get detailed statistics for a platform. + + Returns counts for vendors, pages, and content breakdown. + """ + platform = db.query(Platform).filter(Platform.code == code).first() + + if not platform: + raise HTTPException(status_code=404, detail=f"Platform not found: {code}") + + # Count vendors + vendor_count = ( + db.query(func.count(VendorPlatform.vendor_id)) + .filter(VendorPlatform.platform_id == platform.id) + .scalar() + or 0 + ) + + # Count platform marketing pages + platform_pages_count = ( + db.query(func.count(ContentPage.id)) + .filter( + ContentPage.platform_id == platform.id, + ContentPage.vendor_id == None, + ContentPage.is_platform_page == True, + ) + .scalar() + or 0 + ) + + # Count vendor default pages + vendor_defaults_count = ( + db.query(func.count(ContentPage.id)) + .filter( + ContentPage.platform_id == platform.id, + ContentPage.vendor_id == None, + ContentPage.is_platform_page == False, + ) + .scalar() + or 0 + ) + + # Count vendor override pages + vendor_overrides_count = ( + db.query(func.count(ContentPage.id)) + .filter( + ContentPage.platform_id == platform.id, + ContentPage.vendor_id != None, + ) + .scalar() + or 0 + ) + + # Count published pages + published_pages_count = ( + db.query(func.count(ContentPage.id)) + .filter( + ContentPage.platform_id == platform.id, + ContentPage.is_published == True, + ) + .scalar() + or 0 + ) + + # Count draft pages + draft_pages_count = ( + db.query(func.count(ContentPage.id)) + .filter( + ContentPage.platform_id == platform.id, + ContentPage.is_published == False, + ) + .scalar() + or 0 + ) + + return PlatformStatsResponse( + platform_id=platform.id, + platform_code=platform.code, + platform_name=platform.name, + vendor_count=vendor_count, + platform_pages_count=platform_pages_count, + vendor_defaults_count=vendor_defaults_count, + vendor_overrides_count=vendor_overrides_count, + published_pages_count=published_pages_count, + draft_pages_count=draft_pages_count, + ) diff --git a/app/api/v1/vendor/content_pages.py b/app/api/v1/vendor/content_pages.py index 07fe5c43..1d22ce49 100644 --- a/app/api/v1/vendor/content_pages.py +++ b/app/api/v1/vendor/content_pages.py @@ -97,6 +97,20 @@ class ContentPageResponse(BaseModel): updated_by: int | None +class CMSUsageResponse(BaseModel): + """Schema for CMS usage statistics.""" + + total_pages: int + custom_pages: int + override_pages: int + pages_limit: int | None + custom_pages_limit: int | None + can_create_page: bool + can_create_custom: bool + usage_percent: float + custom_usage_percent: float + + # ============================================================================ # VENDOR CONTENT PAGES # ============================================================================ @@ -241,3 +255,103 @@ def delete_vendor_page( # Delete with ownership check in service layer content_page_service.delete_vendor_page(db, page_id, current_user.token_vendor_id) db.commit() + + +# ============================================================================ +# CMS USAGE & LIMITS +# ============================================================================ + + +@router.get("/usage", response_model=CMSUsageResponse) +def get_cms_usage( + current_user: User = Depends(get_current_vendor_api), + db: Session = Depends(get_db), +): + """ + Get CMS usage statistics for the vendor. + + Returns page counts and limits based on subscription tier. + """ + from models.database.vendor import Vendor + + vendor = db.query(Vendor).filter(Vendor.id == current_user.token_vendor_id).first() + if not vendor: + return CMSUsageResponse( + total_pages=0, + custom_pages=0, + override_pages=0, + pages_limit=3, + custom_pages_limit=0, + can_create_page=False, + can_create_custom=False, + usage_percent=0, + custom_usage_percent=0, + ) + + # Get vendor's pages + vendor_pages = content_page_service.list_all_vendor_pages( + db, vendor_id=current_user.token_vendor_id, include_unpublished=True + ) + + total_pages = len(vendor_pages) + override_pages = sum(1 for p in vendor_pages if p.is_vendor_override) + custom_pages = total_pages - override_pages + + # Get limits from subscription tier + pages_limit = None + custom_pages_limit = None + if vendor.subscription and vendor.subscription.tier: + pages_limit = vendor.subscription.tier.cms_pages_limit + custom_pages_limit = vendor.subscription.tier.cms_custom_pages_limit + + # Calculate can_create flags + can_create_page = pages_limit is None or total_pages < pages_limit + can_create_custom = custom_pages_limit is None or custom_pages < custom_pages_limit + + # Calculate usage percentages + usage_percent = 0 if pages_limit is None else min(100, (total_pages / pages_limit) * 100) if pages_limit > 0 else 100 + custom_usage_percent = 0 if custom_pages_limit is None else min(100, (custom_pages / custom_pages_limit) * 100) if custom_pages_limit > 0 else 100 + + return CMSUsageResponse( + total_pages=total_pages, + custom_pages=custom_pages, + override_pages=override_pages, + pages_limit=pages_limit, + custom_pages_limit=custom_pages_limit, + can_create_page=can_create_page, + can_create_custom=can_create_custom, + usage_percent=usage_percent, + custom_usage_percent=custom_usage_percent, + ) + + +@router.get("/platform-default/{slug}", response_model=ContentPageResponse) +def get_platform_default( + slug: str, + current_user: User = Depends(get_current_vendor_api), + db: Session = Depends(get_db), +): + """ + Get the platform default content for a slug. + + Useful for vendors to view the original before/after overriding. + """ + from models.database.vendor import Vendor + + # Get vendor's platform + vendor = db.query(Vendor).filter(Vendor.id == current_user.token_vendor_id).first() + platform_id = 1 # Default to OMS + + if vendor and vendor.platforms: + platform_id = vendor.platforms[0].id + + # Get platform default (vendor_id=None) + page = content_page_service.get_vendor_default_page( + db, platform_id=platform_id, slug=slug, include_unpublished=True + ) + + if not page: + from app.exceptions import NotFoundException + raise NotFoundException(f"No platform default found for slug: {slug}") + + return page.to_dict() diff --git a/app/routes/admin_pages.py b/app/routes/admin_pages.py index 0c4f0b70..89b1cd6e 100644 --- a/app/routes/admin_pages.py +++ b/app/routes/admin_pages.py @@ -1039,6 +1039,78 @@ async def admin_logs_page( ) +# ============================================================================ +# PLATFORM MANAGEMENT ROUTES (Multi-Platform Support) +# ============================================================================ + + +@router.get("/platforms", response_class=HTMLResponse, include_in_schema=False) +async def admin_platforms_list( + request: Request, + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render platforms management page. + Shows all platforms (OMS, Loyalty, etc.) with their configuration. + """ + return templates.TemplateResponse( + "admin/platforms.html", + { + "request": request, + "user": current_user, + }, + ) + + +@router.get( + "/platforms/{platform_code}", response_class=HTMLResponse, include_in_schema=False +) +async def admin_platform_detail( + request: Request, + platform_code: str = Path(..., description="Platform code (oms, loyalty, etc.)"), + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render platform detail page. + Shows platform configuration, marketing pages, and vendor defaults. + """ + return templates.TemplateResponse( + "admin/platform-detail.html", + { + "request": request, + "user": current_user, + "platform_code": platform_code, + }, + ) + + +@router.get( + "/platforms/{platform_code}/edit", + response_class=HTMLResponse, + include_in_schema=False, +) +async def admin_platform_edit( + request: Request, + platform_code: str = Path(..., description="Platform code"), + current_user: User = Depends(get_current_admin_from_cookie_or_header), + db: Session = Depends(get_db), +): + """ + Render platform edit form. + Allows editing platform settings, branding, and configuration. + """ + return templates.TemplateResponse( + "admin/platform-edit.html", + { + "request": request, + "user": current_user, + "platform_code": platform_code, + }, + ) + + # ============================================================================ # CONTENT MANAGEMENT SYSTEM (CMS) ROUTES # ============================================================================ diff --git a/app/routes/platform_pages.py b/app/routes/platform_pages.py index 587969b4..bc1ccff5 100644 --- a/app/routes/platform_pages.py +++ b/app/routes/platform_pages.py @@ -33,6 +33,10 @@ def get_platform_context(request: Request, db: Session) -> dict: # Get language from request state (set by middleware) language = getattr(request.state, "language", "fr") + # Get platform from middleware (default to OMS platform_id=1) + platform = getattr(request.state, "platform", None) + platform_id = platform.id if platform else 1 + # Get translation function i18n_globals = get_jinja2_globals(language) @@ -52,16 +56,16 @@ def get_platform_context(request: Request, db: Session) -> dict: footer_pages = [] legal_pages = [] try: - # Platform pages have vendor_id=None - header_pages = content_page_service.list_pages_for_vendor( - db, vendor_id=None, header_only=True, include_unpublished=False + # Platform marketing pages (is_platform_page=True) + header_pages = content_page_service.list_platform_pages( + db, platform_id=platform_id, header_only=True, include_unpublished=False ) - footer_pages = content_page_service.list_pages_for_vendor( - db, vendor_id=None, footer_only=True, include_unpublished=False - ) - legal_pages = content_page_service.list_pages_for_vendor( - db, vendor_id=None, legal_only=True, include_unpublished=False + footer_pages = content_page_service.list_platform_pages( + db, platform_id=platform_id, footer_only=True, include_unpublished=False ) + # For legal pages, we need to add footer support or use a different approach + # For now, legal pages come from footer pages with show_in_legal flag + legal_pages = [] # Will be handled separately if needed logger.debug( f"Loaded CMS pages: {len(header_pages)} header, {len(footer_pages)} footer, {len(legal_pages)} legal" ) @@ -307,11 +311,15 @@ async def content_page( Serve CMS content pages (about, contact, faq, privacy, terms, etc.). This is a catch-all route for dynamic content pages managed via the admin CMS. - Platform pages have vendor_id=None. + Platform pages have vendor_id=None and is_platform_page=True. """ - # Load content page from database (platform defaults only) - page = content_page_service.get_page_for_vendor( - db, slug=slug, vendor_id=None, include_unpublished=False + # Get platform from middleware (default to OMS platform_id=1) + platform = getattr(request.state, "platform", None) + platform_id = platform.id if platform else 1 + + # Load platform marketing page from database + page = content_page_service.get_platform_page( + db, platform_id=platform_id, slug=slug, include_unpublished=False ) if not page: diff --git a/app/routes/shop_pages.py b/app/routes/shop_pages.py index a548151c..ea82d35f 100644 --- a/app/routes/shop_pages.py +++ b/app/routes/shop_pages.py @@ -114,10 +114,14 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d """ # Extract from middleware state vendor = getattr(request.state, "vendor", None) + platform = getattr(request.state, "platform", None) theme = getattr(request.state, "theme", None) clean_path = getattr(request.state, "clean_path", request.url.path) vendor_context = getattr(request.state, "vendor_context", None) + # Get platform_id (default to 1 for OMS if not set) + platform_id = platform.id if platform else 1 + # Get detection method from vendor_context access_method = ( vendor_context.get("detection_method", "unknown") @@ -156,11 +160,11 @@ def get_shop_context(request: Request, db: Session = None, **extra_context) -> d vendor_id = vendor.id # Get pages configured to show in footer footer_pages = content_page_service.list_pages_for_vendor( - db, vendor_id=vendor_id, footer_only=True, include_unpublished=False + db, platform_id=platform_id, vendor_id=vendor_id, footer_only=True, include_unpublished=False ) # Get pages configured to show in header header_pages = content_page_service.list_pages_for_vendor( - db, vendor_id=vendor_id, header_only=True, include_unpublished=False + db, platform_id=platform_id, vendor_id=vendor_id, header_only=True, include_unpublished=False ) except Exception as e: logger.error( @@ -752,11 +756,13 @@ async def generic_content_page( ) vendor = getattr(request.state, "vendor", None) + platform = getattr(request.state, "platform", None) vendor_id = vendor.id if vendor else None + platform_id = platform.id if platform else 1 # Default to OMS - # Load content page from database (vendor override → platform default) + # Load content page from database (vendor override → vendor default) page = content_page_service.get_page_for_vendor( - db, slug=slug, vendor_id=vendor_id, include_unpublished=False + db, platform_id=platform_id, slug=slug, vendor_id=vendor_id, include_unpublished=False ) if not page: diff --git a/app/templates/admin/content-pages.html b/app/templates/admin/content-pages.html index ece4df9e..09e44c5d 100644 --- a/app/templates/admin/content-pages.html +++ b/app/templates/admin/content-pages.html @@ -17,25 +17,45 @@
-
+
{% call tabs_inline() %} {{ tab_button('all', 'All Pages', count_var='allPages.length') }} - {{ tab_button('platform', 'Platform Defaults', count_var='platformPages.length') }} - {{ tab_button('vendor', 'Vendor Overrides', count_var='vendorPages.length') }} + {{ tab_button('platform_marketing', 'Platform Marketing', count_var='platformMarketingPages.length') }} + {{ tab_button('vendor_defaults', 'Vendor Defaults', count_var='vendorDefaultPages.length') }} + {{ tab_button('vendor_overrides', 'Vendor Overrides', count_var='vendorOverridePages.length') }} {% endcall %} - -
- - - - + +
+ +
+ + + + +
+ + +
+ + + + +
@@ -73,12 +93,18 @@ - + + + diff --git a/app/templates/admin/partials/sidebar.html b/app/templates/admin/partials/sidebar.html index 4dcb5265..3304cb61 100644 --- a/app/templates/admin/partials/sidebar.html +++ b/app/templates/admin/partials/sidebar.html @@ -104,6 +104,7 @@ {{ section_header('Content Management', 'contentMgmt') }} {% call section_content('contentMgmt') %} + {{ menu_item('platforms', '/admin/platforms', 'globe-alt', 'Platforms') }} {{ menu_item('platform-homepage', '/admin/platform-homepage', 'home', 'Platform Homepage') }} {{ menu_item('content-pages', '/admin/content-pages', 'document-text', 'Content Pages') }} {{ menu_item('vendor-theme', '/admin/vendor-themes', 'color-swatch', 'Vendor Themes') }} diff --git a/app/templates/admin/platforms.html b/app/templates/admin/platforms.html new file mode 100644 index 00000000..63efaa2e --- /dev/null +++ b/app/templates/admin/platforms.html @@ -0,0 +1,154 @@ +{# app/templates/admin/platforms.html #} +{% extends "admin/base.html" %} +{% from 'shared/macros/headers.html' import page_header %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} + +{% block title %}Platforms{% endblock %} + +{% block alpine_data %}platformsManager(){% endblock %} + +{% block content %} +{{ page_header('Platforms', subtitle='Manage platform configurations for OMS, Loyalty, and other business offerings') }} + +{{ loading_state('Loading platforms...') }} + +{{ error_state('Error loading platforms') }} + + +
+ +
+ + +
+ +

No platforms found

+

+ No platforms have been configured yet. +

+
+ + +
+

Content Page Tiers

+
+
+ +
+

Platform Marketing Pages

+

Public pages like homepage, pricing, features. Not inherited by vendors.

+
+
+
+ +
+

Vendor Defaults

+

Default pages inherited by all vendors (about, terms, privacy).

+
+
+
+ +
+

Vendor Overrides

+

Custom pages created by individual vendors.

+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/vendor/content-page-edit.html b/app/templates/vendor/content-page-edit.html index 399c3dd7..7a56e547 100644 --- a/app/templates/vendor/content-page-edit.html +++ b/app/templates/vendor/content-page-edit.html @@ -41,14 +41,73 @@
-
- -
-

Overriding Platform Default

-

- You're customizing the "" page. Your version will be shown to customers instead of the platform default. - -

+
+
+ +
+

Overriding Platform Default

+

+ You're customizing the "" page. Your version will be shown to customers instead of the platform default. +

+
+
+
+ + +
+
+
+ + +
+
+
+

Platform Default Content

+ +
+
+
+ +

Loading default content...

+
+
+

Title

+

+

Content

+
+
+
+
+
diff --git a/app/templates/vendor/content-pages.html b/app/templates/vendor/content-pages.html index e5e6d623..c19bc8af 100644 --- a/app/templates/vendor/content-pages.html +++ b/app/templates/vendor/content-pages.html @@ -15,6 +15,50 @@ {{ error_state('Error loading pages') }} + +
+
+
+
+ CMS Usage + + + / + (unlimited) + pages + +
+ +
+
+
+
+ overrides + custom pages +
+
+ + + +
+
+
diff --git a/docs/architecture/multi-platform-cms.md b/docs/architecture/multi-platform-cms.md new file mode 100644 index 00000000..9e996694 --- /dev/null +++ b/docs/architecture/multi-platform-cms.md @@ -0,0 +1,239 @@ +# Multi-Platform CMS Architecture + +## Overview + +The Multi-Platform CMS enables Wizamart to serve multiple business offerings (OMS, Loyalty, Site Builder) from a single codebase, each with its own marketing site and vendor ecosystem. + +## Three-Tier Content Hierarchy + +Content pages follow a three-tier inheritance model: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ TIER 1: Platform Marketing │ +│ Public pages for the platform (homepage, pricing, features) │ +│ is_platform_page=TRUE, vendor_id=NULL │ +│ NOT inherited by vendors │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ TIER 2: Vendor Defaults │ +│ Default pages all vendors inherit (about, terms, privacy) │ +│ is_platform_page=FALSE, vendor_id=NULL │ +│ Inherited by ALL vendors on the platform │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ TIER 3: Vendor Overrides │ +│ Custom pages created by individual vendors │ +│ is_platform_page=FALSE, vendor_id= │ +│ Overrides vendor defaults for specific vendor │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Content Resolution Flow + +When a customer visits a vendor page (e.g., `/vendors/shopname/about`): + +``` +Customer visits: /vendors/shopname/about + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Step 1: Check Vendor Override │ +│ SELECT * FROM content_pages │ +│ WHERE platform_id=1 AND vendor_id=123 AND slug='about' │ +│ Found? → Return vendor's custom "About" page │ +└─────────────────────────────────────────────────────────────────────┘ + │ Not found + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Step 2: Check Vendor Default │ +│ SELECT * FROM content_pages │ +│ WHERE platform_id=1 AND vendor_id IS NULL │ +│ AND is_platform_page=FALSE AND slug='about' │ +│ Found? → Return platform's default "About" template │ +└─────────────────────────────────────────────────────────────────────┘ + │ Not found + ▼ + Return 404 +``` + +## Database Schema + +### platforms + +```sql +CREATE TABLE platforms ( + id SERIAL PRIMARY KEY, + code VARCHAR(50) UNIQUE NOT NULL, -- 'oms', 'loyalty', 'sitebuilder' + name VARCHAR(100) NOT NULL, -- 'Order Management System' + description TEXT, + domain VARCHAR(255), -- 'oms.wizamart.lu' + path_prefix VARCHAR(50), -- '/oms' + logo VARCHAR(255), + logo_dark VARCHAR(255), + favicon VARCHAR(255), + theme_config JSONB DEFAULT '{}', + default_language VARCHAR(10) DEFAULT 'fr', + supported_languages JSONB DEFAULT '["fr", "de", "en"]', + is_active BOOLEAN DEFAULT TRUE, + is_public BOOLEAN DEFAULT TRUE, + settings JSONB DEFAULT '{}', + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +### vendor_platforms (Junction Table) + +```sql +CREATE TABLE vendor_platforms ( + vendor_id INTEGER REFERENCES vendors(id) ON DELETE CASCADE, + platform_id INTEGER REFERENCES platforms(id) ON DELETE CASCADE, + joined_at TIMESTAMP DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + settings JSONB DEFAULT '{}', + PRIMARY KEY (vendor_id, platform_id) +); +``` + +### content_pages (Extended) + +```sql +ALTER TABLE content_pages ADD COLUMN platform_id INTEGER REFERENCES platforms(id); +ALTER TABLE content_pages ADD COLUMN is_platform_page BOOLEAN DEFAULT FALSE; + +-- Platform marketing pages: is_platform_page=TRUE, vendor_id=NULL +-- Vendor defaults: is_platform_page=FALSE, vendor_id=NULL +-- Vendor overrides: is_platform_page=FALSE, vendor_id= +``` + +## Request Flow + +``` +Request: GET /oms/vendors/shopname/about + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ PlatformContextMiddleware │ +│ - Detects platform from path prefix (/oms) or domain │ +│ - Sets request.state.platform = Platform(code='oms') │ +│ - Sets request.state.platform_clean_path = /vendors/shopname/about │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ VendorContextMiddleware │ +│ - Uses platform_clean_path for vendor detection │ +│ - Sets request.state.vendor = Vendor(subdomain='shopname') │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Route Handler (shop_pages.py) │ +│ - Gets platform_id from request.state.platform │ +│ - Calls content_page_service.get_page_for_vendor( │ +│ platform_id=1, vendor_id=123, slug='about' │ +│ ) │ +│ - Service handles three-tier resolution │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Admin Interface + +### Platform Management (`/admin/platforms`) + +- Lists all platforms with statistics +- Shows vendor count, marketing pages, vendor defaults +- Links to platform detail and edit pages + +### Content Pages (`/admin/content-pages`) + +- Platform filter dropdown +- Four-tab view: + - **All Pages**: Complete list + - **Platform Marketing**: Public platform pages (is_platform_page=TRUE) + - **Vendor Defaults**: Inherited by vendors (is_platform_page=FALSE, vendor_id=NULL) + - **Vendor Overrides**: Vendor-specific (vendor_id set) +- Color-coded tier badges: + - Blue: Platform Marketing + - Teal: Vendor Default + - Purple: Vendor Override + +## API Endpoints + +### Platform Management + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/v1/admin/platforms` | List all platforms | +| GET | `/api/v1/admin/platforms/{code}` | Get platform details | +| PUT | `/api/v1/admin/platforms/{code}` | Update platform | +| GET | `/api/v1/admin/platforms/{code}/stats` | Platform statistics | + +### Content Pages + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/v1/admin/content-pages/` | List all pages (supports `platform` filter) | +| GET | `/api/v1/admin/content-pages/platform` | Platform default pages only | +| POST | `/api/v1/admin/content-pages/platform` | Create platform page | +| POST | `/api/v1/admin/content-pages/vendor` | Create vendor page | + +## Key Files + +### Models +- `models/database/platform.py` - Platform model +- `models/database/vendor_platform.py` - Junction table +- `models/database/content_page.py` - Extended with platform_id + +### Middleware +- `middleware/platform_context.py` - Platform detection and context + +### Services +- `app/services/content_page_service.py` - Three-tier content resolution + +### Routes +- `app/routes/platform_pages.py` - Platform marketing pages +- `app/routes/shop_pages.py` - Vendor shop pages with inheritance + +### Admin +- `app/api/v1/admin/platforms.py` - Platform management API +- `app/templates/admin/platforms.html` - Platform admin UI +- `static/admin/js/platforms.js` - Alpine.js component + +## CMS Tier Limits (Subscription-Based) + +| Tier | Total Pages | Custom Pages | +|------|-------------|--------------| +| Essential | 3 | 0 | +| Professional | 10 | 5 | +| Business | 30 | 20 | +| Enterprise | Unlimited | Unlimited | + +## Adding a New Platform + +1. Insert platform record: + ```sql + INSERT INTO platforms (code, name, description, path_prefix) + VALUES ('loyalty', 'Loyalty Program', 'Customer loyalty and rewards', '/loyalty'); + ``` + +2. Create platform-specific content pages: + ```sql + INSERT INTO content_pages (platform_id, slug, title, content, is_platform_page) + VALUES (2, 'home', 'Loyalty Program', '

Welcome

', TRUE); + ``` + +3. Configure routing (if using path prefix): + - Platform detected by `PlatformContextMiddleware` + - No additional route configuration needed + +4. Assign vendors to platform: + ```sql + INSERT INTO vendor_platforms (vendor_id, platform_id) + VALUES (1, 2); + ``` diff --git a/docs/proposals/multi-platform-cms-architecture-implementation-plan.md b/docs/proposals/multi-platform-cms-architecture-implementation-plan.md index 8e401976..20fcbe8c 100644 --- a/docs/proposals/multi-platform-cms-architecture-implementation-plan.md +++ b/docs/proposals/multi-platform-cms-architecture-implementation-plan.md @@ -1,8 +1,12 @@ # Multi-Platform CMS Architecture - Implementation Plan -> **Status:** Phase 1 Complete | Phases 2-6 Pending -> **Last Updated:** 2026-01-18 -> **Commit:** `408019d` (feat: add multi-platform CMS architecture Phase 1) +> **Status:** Phases 1-4 Complete | Phase 5 Pending +> **Last Updated:** 2026-01-19 +> **Commits:** +> - `408019d` (Phase 1: Database & Models) +> - Phase 2: OMS Migration & Integration +> - Phase 3: Admin Interface +> - Phase 4: Vendor Dashboard --- @@ -32,7 +36,7 @@ Transform the single-platform OMS into a multi-platform system supporting indepe --- -## Phase 2: OMS Migration & Integration 🔄 NEXT +## Phase 2: OMS Migration & Integration ✅ COMPLETE ### 2.1 Run Database Migration @@ -109,42 +113,57 @@ Files to update: --- -## Phase 3: Admin Interface +## Phase 3: Admin Interface ✅ COMPLETE ### 3.1 Platform Management UI -New routes needed: -- [ ] `GET /admin/platforms` - List all platforms -- [ ] `GET /admin/platforms/{code}` - Platform details -- [ ] `GET /admin/platforms/{code}/pages` - Platform marketing pages -- [ ] `GET /admin/platforms/{code}/defaults` - Vendor default pages -- [ ] `POST/PUT/DELETE` endpoints for CRUD operations +| Task | File | Status | +|------|------|--------| +| Platform list page route | `app/routes/admin_pages.py` | ✅ | +| Platform detail/edit routes | `app/routes/admin_pages.py` | ✅ | +| Platform API endpoints | `app/api/v1/admin/platforms.py` | ✅ | +| Register API router | `app/api/v1/admin/__init__.py` | ✅ | +| Platforms template | `app/templates/admin/platforms.html` | ✅ | +| Platforms JS component | `static/admin/js/platforms.js` | ✅ | +| Sidebar menu item | `app/templates/admin/partials/sidebar.html` | ✅ | ### 3.2 Update Content Pages Admin -Changes to existing admin: -- [ ] Add platform dropdown filter -- [ ] Show page tier badge (Platform / Default / Override) -- [ ] Add `is_platform_page` toggle for platform-level pages -- [ ] Group pages by tier in list view +| Task | File | Status | +|------|------|--------| +| Platform dropdown filter | `app/templates/admin/content-pages.html` | ✅ | +| Four-tab tier view | `app/templates/admin/content-pages.html` | ✅ | +| Tier badges (Blue/Teal/Purple) | `static/admin/js/content-pages.js` | ✅ | +| Platform filter in JS | `static/admin/js/content-pages.js` | ✅ | +| API schema with platform fields | `app/api/v1/admin/content_pages.py` | ✅ | +| Model to_dict with platform_name | `models/database/content_page.py` | ✅ | --- -## Phase 4: Vendor Dashboard +## Phase 4: Vendor Dashboard ✅ COMPLETE ### 4.1 Content Pages List Updates -- [ ] Show source indicator: "Default" / "Override" / "Custom" -- [ ] Add "Override Default" button for vendor default pages -- [ ] Add "Revert to Default" button for vendor overrides -- [ ] Show CMS usage: "3 of 10 pages used" with progress bar -- [ ] Upgrade prompt when approaching limit +| Task | File | Status | +|------|------|--------| +| Source indicators (Default/Override/Custom) | `app/templates/vendor/content-pages.html` | ✅ Already existed | +| Override Default button | `app/templates/vendor/content-pages.html` | ✅ Already existed | +| Revert to Default (delete override) | `static/vendor/js/content-pages.js` | ✅ Already existed | +| CMS usage API endpoint | `app/api/v1/vendor/content_pages.py` | ✅ New | +| CMS usage progress bar | `app/templates/vendor/content-pages.html` | ✅ New | +| Upgrade prompt at 80% limit | `app/templates/vendor/content-pages.html` | ✅ New | +| Load usage in JS | `static/vendor/js/content-pages.js` | ✅ New | ### 4.2 Page Editor Updates -- [ ] Show banner: "This page overrides the platform default" -- [ ] "View Default" button to preview default content -- [ ] "Revert" button inline in editor +| Task | File | Status | +|------|------|--------| +| Override info banner | `app/templates/vendor/content-page-edit.html` | ✅ Already existed | +| View Default button | `app/templates/vendor/content-page-edit.html` | ✅ New | +| Default preview modal | `app/templates/vendor/content-page-edit.html` | ✅ New | +| Platform default API | `app/api/v1/vendor/content_pages.py` | ✅ New | +| Show default preview JS | `static/vendor/js/content-page-edit.js` | ✅ New | +| Revert button (styled) | `app/templates/vendor/content-page-edit.html` | ✅ New | --- @@ -202,30 +221,32 @@ VALUES ('loyalty', 'Loyalty+', 'Customer loyalty program', 'loyalty.lu', 'loyalt --- -## Documentation Requirements +## Documentation Requirements ✅ PARTIAL -### Architecture Documentation +### Architecture Documentation ✅ COMPLETE -Create `docs/architecture/multi-platform-cms.md`: -- [ ] Three-tier content hierarchy explanation -- [ ] Platform vs Vendor Default vs Vendor Override -- [ ] Database schema diagrams -- [ ] Request flow diagrams +Created `docs/architecture/multi-platform-cms.md`: +- [x] Three-tier content hierarchy explanation +- [x] Platform vs Vendor Default vs Vendor Override +- [x] Database schema diagrams +- [x] Request flow diagrams +- [x] API endpoints reference +- [x] Key files reference +- [x] Adding new platform guide ### API Documentation -Update OpenAPI specs: -- [ ] Platform endpoints -- [ ] Content page endpoints with platform_id -- [ ] Vendor platform membership endpoints +OpenAPI specs auto-generated from FastAPI: +- [x] Platform endpoints (`/api/v1/admin/platforms`) +- [x] Content page endpoints with platform fields +- [ ] Vendor platform membership endpoints (future) ### Developer Guide -Create `docs/guides/adding-new-platform.md`: -- [ ] Step-by-step platform creation -- [ ] Required database records -- [ ] Required config files -- [ ] Required routes and templates +Included in `docs/architecture/multi-platform-cms.md`: +- [x] Step-by-step platform creation +- [x] Required database records +- [x] Key files reference --- diff --git a/models/database/content_page.py b/models/database/content_page.py index 88e41c8d..585a1fa3 100644 --- a/models/database/content_page.py +++ b/models/database/content_page.py @@ -191,6 +191,7 @@ class ContentPage(Base): "id": self.id, "platform_id": self.platform_id, "platform_code": self.platform.code if self.platform else None, + "platform_name": self.platform.name if self.platform else None, "vendor_id": self.vendor_id, "vendor_name": self.vendor.name if self.vendor else None, "slug": self.slug, diff --git a/static/admin/js/content-pages.js b/static/admin/js/content-pages.js index f2669d08..d4d651c0 100644 --- a/static/admin/js/content-pages.js +++ b/static/admin/js/content-pages.js @@ -17,12 +17,14 @@ function contentPagesManager() { // Content pages specific state allPages: [], + platforms: [], loading: false, error: null, // Tabs and filters - activeTab: 'all', // all, platform, vendor + activeTab: 'all', // all, platform_marketing, vendor_defaults, vendor_overrides searchQuery: '', + selectedPlatform: '', // Platform code filter // Initialize async init() { @@ -35,43 +37,77 @@ function contentPagesManager() { } window._contentPagesInitialized = true; - contentPagesLog.group('Loading content pages'); - await this.loadPages(); + contentPagesLog.group('Loading data'); + await Promise.all([ + this.loadPages(), + this.loadPlatforms() + ]); contentPagesLog.groupEnd(); + // Check for platform filter in URL + const urlParams = new URLSearchParams(window.location.search); + const platformParam = urlParams.get('platform'); + if (platformParam) { + this.selectedPlatform = platformParam; + } + contentPagesLog.info('=== CONTENT PAGES MANAGER INITIALIZATION COMPLETE ==='); }, - // Computed: Platform pages + // Computed: Platform Marketing pages (is_platform_page=true, vendor_id=null) + get platformMarketingPages() { + return this.allPages.filter(page => page.is_platform_page && !page.vendor_id); + }, + + // Computed: Vendor Default pages (is_platform_page=false, vendor_id=null) + get vendorDefaultPages() { + return this.allPages.filter(page => !page.is_platform_page && !page.vendor_id); + }, + + // Computed: Vendor Override pages (vendor_id is set) + get vendorOverridePages() { + return this.allPages.filter(page => page.vendor_id); + }, + + // Legacy computed (for backward compatibility) get platformPages() { - return this.allPages.filter(page => page.is_platform_default); + return [...this.platformMarketingPages, ...this.vendorDefaultPages]; }, - // Computed: Vendor pages get vendorPages() { - return this.allPages.filter(page => page.is_vendor_override); + return this.vendorOverridePages; }, - // Computed: Filtered pages based on active tab and search + // Computed: Filtered pages based on active tab, platform, and search get filteredPages() { let pages = []; - // Filter by tab - if (this.activeTab === 'platform') { - pages = this.platformPages; - } else if (this.activeTab === 'vendor') { - pages = this.vendorPages; + // Filter by tab (three-tier system) + if (this.activeTab === 'platform_marketing') { + pages = this.platformMarketingPages; + } else if (this.activeTab === 'vendor_defaults') { + pages = this.vendorDefaultPages; + } else if (this.activeTab === 'vendor_overrides') { + pages = this.vendorOverridePages; } else { pages = this.allPages; } + // Filter by selected platform + if (this.selectedPlatform) { + pages = pages.filter(page => + page.platform_code === this.selectedPlatform + ); + } + // Filter by search query if (this.searchQuery) { const query = this.searchQuery.toLowerCase(); pages = pages.filter(page => page.title.toLowerCase().includes(query) || page.slug.toLowerCase().includes(query) || - (page.vendor_name && page.vendor_name.toLowerCase().includes(query)) + (page.vendor_name && page.vendor_name.toLowerCase().includes(query)) || + (page.platform_name && page.platform_name.toLowerCase().includes(query)) ); } @@ -113,6 +149,44 @@ function contentPagesManager() { } }, + // Load platforms for filter dropdown + async loadPlatforms() { + try { + contentPagesLog.info('Fetching platforms...'); + const response = await apiClient.get('/admin/platforms'); + this.platforms = response.platforms || []; + contentPagesLog.info(`Loaded ${this.platforms.length} platforms`); + } catch (err) { + contentPagesLog.error('Error loading platforms:', err); + // Non-critical - don't set error state + } + }, + + // Get page tier label (three-tier system) + getPageTierLabel(page) { + if (page.vendor_id) { + return 'Vendor Override'; + } else if (page.is_platform_page) { + return 'Platform Marketing'; + } else { + return 'Vendor Default'; + } + }, + + // Get page tier CSS class (three-tier system) + getPageTierClass(page) { + if (page.vendor_id) { + // Vendor Override - purple + return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'; + } else if (page.is_platform_page) { + // Platform Marketing - blue + return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; + } else { + // Vendor Default - teal + return 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200'; + } + }, + // Delete a page async deletePage(page) { if (!confirm(`Are you sure you want to delete "${page.title}"?`)) { diff --git a/static/admin/js/platforms.js b/static/admin/js/platforms.js new file mode 100644 index 00000000..636ec9ce --- /dev/null +++ b/static/admin/js/platforms.js @@ -0,0 +1,58 @@ +/** + * Platforms Manager - Alpine.js Component + * + * Handles platform listing and management for multi-platform CMS. + */ + +function platformsManager() { + return { + // State + platforms: [], + loading: true, + error: null, + + // Lifecycle + async init() { + this.currentPage = "platforms"; + await this.loadPlatforms(); + }, + + // API Methods + async loadPlatforms() { + this.loading = true; + this.error = null; + + try { + const response = await apiClient.get("/admin/platforms"); + this.platforms = response.platforms || []; + console.log(`[PLATFORMS] Loaded ${this.platforms.length} platforms`); + } catch (err) { + console.error("[PLATFORMS] Error loading platforms:", err); + this.error = err.message || "Failed to load platforms"; + } finally { + this.loading = false; + } + }, + + // Helper Methods + getPlatformIcon(code) { + const icons = { + oms: "clipboard-list", + loyalty: "star", + sitebuilder: "template", + default: "globe-alt", + }; + return icons[code] || icons.default; + }, + + formatDate(dateString) { + if (!dateString) return "—"; + const date = new Date(dateString); + return date.toLocaleDateString("fr-LU", { + year: "numeric", + month: "short", + day: "numeric", + }); + }, + }; +} diff --git a/static/vendor/js/content-page-edit.js b/static/vendor/js/content-page-edit.js index 4488d30a..f7c85fa1 100644 --- a/static/vendor/js/content-page-edit.js +++ b/static/vendor/js/content-page-edit.js @@ -35,6 +35,11 @@ function vendorContentPageEditor(pageId) { error: null, successMessage: null, + // Default preview modal state + showingDefaultPreview: false, + loadingDefault: false, + defaultContent: null, + // Initialize async init() { // Prevent multiple initializations @@ -179,6 +184,31 @@ function vendorContentPageEditor(pageId) { } }, + // Show default content preview + async showDefaultPreview() { + this.showingDefaultPreview = true; + this.loadingDefault = true; + this.defaultContent = null; + + try { + contentPageEditLog.info('Loading platform default for slug:', this.form.slug); + + const response = await apiClient.get(`/vendor/content-pages/platform-default/${this.form.slug}`); + this.defaultContent = response.data || response; + + contentPageEditLog.info('Default content loaded'); + + } catch (err) { + contentPageEditLog.error('Error loading default content:', err); + this.defaultContent = { + title: 'Error', + content: `

Failed to load platform default: ${err.message}

` + }; + } finally { + this.loadingDefault = false; + } + }, + // Delete page (revert to default for overrides) async deletePage() { const message = this.isOverride diff --git a/static/vendor/js/content-pages.js b/static/vendor/js/content-pages.js index 54cbd665..861ea603 100644 --- a/static/vendor/js/content-pages.js +++ b/static/vendor/js/content-pages.js @@ -24,6 +24,7 @@ function vendorContentPagesManager() { platformPages: [], // Platform default pages customPages: [], // Vendor's own pages (overrides + custom) overrideMap: {}, // Map of slug -> page id for quick lookup + cmsUsage: null, // CMS usage statistics // Initialize async init() { @@ -43,7 +44,10 @@ function vendorContentPagesManager() { await parentInit.call(this); } - await this.loadPages(); + await Promise.all([ + this.loadPages(), + this.loadCmsUsage() + ]); contentPagesLog.info('=== VENDOR CONTENT PAGES MANAGER INITIALIZATION COMPLETE ==='); } catch (error) { @@ -92,6 +96,19 @@ function vendorContentPagesManager() { } }, + // Load CMS usage statistics + async loadCmsUsage() { + try { + contentPagesLog.info('Loading CMS usage...'); + const response = await apiClient.get('/vendor/content-pages/usage'); + this.cmsUsage = response.data || response; + contentPagesLog.info('CMS usage loaded:', this.cmsUsage); + } catch (err) { + contentPagesLog.error('Error loading CMS usage:', err); + // Non-critical - don't set error state + } + }, + // Check if vendor has overridden a platform page hasOverride(slug) { return slug in this.overrideMap;