Some checks failed
Storefront homepage & module gating:
- CMS owns storefront GET / (slug="home" with 3-tier resolution)
- Catalog loses GET / (keeps /products only)
- Store root redirect (GET / → /store/dashboard or /store/login)
- Route gating: non-core modules return 404 when disabled for platform
- Seed store default homepages per platform
Widget protocol for customer dashboard:
- StorefrontDashboardCard contract in widgets.py
- Widget aggregator get_storefront_dashboard_cards()
- Orders and Loyalty module widget providers
- Dashboard template renders contributed cards (no module names)
Landing template module-agnostic:
- CTAs driven by storefront_nav (not hardcoded module names)
- Header actions check nav item IDs (not enabled_modules)
- Remove hardcoded "Add Product" sidebar button
- Remove all enabled_modules checks from storefront templates
i18n fixes:
- Title placeholder resolution ({{store_name}}) for store default pages
- Storefront nav label_keys prefixed with module code
- Add storefront.account.* keys to 6 modules (en/fr/de/lb)
- Header/footer CMS pages use get_translated_title(current_language)
- Footer labels use i18n keys instead of hardcoded English
Icon cleanup:
- Standardize on map-pin (remove location-marker alias)
- Replace all location-marker references across templates and docs
Docs:
- Storefront builder vision proposal (6 phases)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
413 lines
12 KiB
Python
413 lines
12 KiB
Python
# app/modules/contracts/widgets.py
|
|
"""
|
|
Dashboard widget provider protocol for cross-module widget aggregation.
|
|
|
|
This module defines the protocol that modules implement to expose dashboard widgets.
|
|
The core module's WidgetAggregator discovers and aggregates all providers.
|
|
|
|
Benefits:
|
|
- Each module owns its widgets (no cross-module coupling)
|
|
- Dashboards always work (aggregator is in core)
|
|
- Optional modules are truly optional (can be removed without breaking app)
|
|
- Easy to add new widgets (just implement protocol in your module)
|
|
|
|
Widget Types:
|
|
- ListWidget: Displays a list of items (recent imports, recent stores, etc.)
|
|
- BreakdownWidget: Displays grouped statistics (by category, by status, etc.)
|
|
|
|
Usage:
|
|
# 1. Implement the protocol in your module
|
|
class OrderWidgetProvider:
|
|
@property
|
|
def widgets_category(self) -> str:
|
|
return "orders"
|
|
|
|
def get_store_widgets(self, db, store_id, context=None) -> list[DashboardWidget]:
|
|
return [
|
|
DashboardWidget(
|
|
key="orders.recent",
|
|
widget_type="list",
|
|
title="Recent Orders",
|
|
category="orders",
|
|
data=ListWidget(items=[...]),
|
|
)
|
|
]
|
|
|
|
# 2. Register in module definition
|
|
def _get_widget_provider():
|
|
from app.modules.orders.services.order_widgets import order_widget_provider
|
|
return order_widget_provider
|
|
|
|
orders_module = ModuleDefinition(
|
|
code="orders",
|
|
widget_provider=_get_widget_provider,
|
|
# ...
|
|
)
|
|
|
|
# 3. Widgets appear automatically in dashboards when module is enabled
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
|
|
if TYPE_CHECKING:
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
# =============================================================================
|
|
# Context
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class WidgetContext:
|
|
"""
|
|
Context for widget data collection.
|
|
|
|
Provides filtering and scoping options for widget providers.
|
|
|
|
Attributes:
|
|
date_from: Start of date range filter
|
|
date_to: End of date range filter
|
|
limit: Maximum number of items for list widgets
|
|
include_details: Whether to include extra details (may be expensive)
|
|
"""
|
|
|
|
date_from: datetime | None = None
|
|
date_to: datetime | None = None
|
|
limit: int = 5
|
|
include_details: bool = False
|
|
|
|
|
|
# =============================================================================
|
|
# Storefront Dashboard Card
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class StorefrontDashboardCard:
|
|
"""
|
|
A card contributed by a module to the storefront customer dashboard.
|
|
|
|
Modules implement get_storefront_dashboard_cards() to provide these.
|
|
The dashboard template renders them without knowing which module provided them.
|
|
|
|
Attributes:
|
|
key: Unique identifier (e.g. "orders.summary", "loyalty.points")
|
|
icon: Lucide icon name (e.g. "shopping-bag", "gift")
|
|
title: Card title (i18n key or plain text)
|
|
subtitle: Card subtitle / description
|
|
route: Link destination relative to base_url (e.g. "account/orders")
|
|
value: Primary display value (e.g. order count, points balance)
|
|
value_label: Label for the value (e.g. "Total Orders", "Points Balance")
|
|
order: Sort order (lower = shown first)
|
|
template: Optional custom template path for complex rendering
|
|
extra_data: Additional data for custom template rendering
|
|
"""
|
|
|
|
key: str
|
|
icon: str
|
|
title: str
|
|
subtitle: str
|
|
route: str
|
|
value: str | int | None = None
|
|
value_label: str | None = None
|
|
order: int = 100
|
|
template: str | None = None
|
|
extra_data: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
# =============================================================================
|
|
# Widget Item Types
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class WidgetListItem:
|
|
"""
|
|
Single item in a list widget (recent stores, orders, imports).
|
|
|
|
Attributes:
|
|
id: Unique identifier for the item
|
|
title: Primary display text
|
|
subtitle: Secondary display text (optional)
|
|
status: Status indicator ("success", "warning", "error", "neutral")
|
|
timestamp: When the item was created/updated
|
|
url: Link to item details (optional)
|
|
metadata: Additional data for rendering
|
|
"""
|
|
|
|
id: int | str
|
|
title: str
|
|
subtitle: str | None = None
|
|
status: str | None = None # "success", "warning", "error", "neutral"
|
|
timestamp: datetime | None = None
|
|
url: str | None = None
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class WidgetBreakdownItem:
|
|
"""
|
|
Single row in a breakdown widget (stats by marketplace, category).
|
|
|
|
Attributes:
|
|
label: Display label for the row
|
|
value: Primary numeric value
|
|
secondary_value: Optional secondary value (e.g., comparison)
|
|
percentage: Percentage representation
|
|
icon: Lucide icon name (optional)
|
|
"""
|
|
|
|
label: str
|
|
value: int | float
|
|
secondary_value: int | float | None = None
|
|
percentage: float | None = None
|
|
icon: str | None = None
|
|
|
|
|
|
# =============================================================================
|
|
# Widget Containers
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class ListWidget:
|
|
"""
|
|
Widget containing a list of items.
|
|
|
|
Used for: recent imports, recent stores, recent orders, etc.
|
|
|
|
Attributes:
|
|
items: List of WidgetListItem objects
|
|
total_count: Total number of items (for "X more" display)
|
|
view_all_url: Link to view all items
|
|
"""
|
|
|
|
items: list[WidgetListItem]
|
|
total_count: int | None = None
|
|
view_all_url: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class BreakdownWidget:
|
|
"""
|
|
Widget containing grouped statistics.
|
|
|
|
Used for: status breakdown, category distribution, etc.
|
|
|
|
Attributes:
|
|
items: List of WidgetBreakdownItem objects
|
|
total: Sum of all values
|
|
"""
|
|
|
|
items: list[WidgetBreakdownItem]
|
|
total: int | float | None = None
|
|
|
|
|
|
# Type alias for widget data
|
|
WidgetData = ListWidget | BreakdownWidget
|
|
|
|
|
|
# =============================================================================
|
|
# Main Widget Envelope
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class DashboardWidget:
|
|
"""
|
|
Standard widget envelope with metadata.
|
|
|
|
This is the main unit of data returned by widget providers.
|
|
It contains both the widget data and metadata for display.
|
|
|
|
Attributes:
|
|
key: Unique identifier for this widget (e.g., "marketplace.recent_imports")
|
|
Format: "{category}.{widget_name}" for consistency
|
|
widget_type: Type of widget ("list" or "breakdown")
|
|
title: Human-readable title for display
|
|
category: Grouping category (should match widgets_category of provider)
|
|
data: The actual widget data (ListWidget or BreakdownWidget)
|
|
icon: Optional Lucide icon name for UI display
|
|
description: Optional longer description
|
|
order: Display order (lower = higher priority)
|
|
|
|
Example:
|
|
DashboardWidget(
|
|
key="marketplace.recent_imports",
|
|
widget_type="list",
|
|
title="Recent Imports",
|
|
category="marketplace",
|
|
data=ListWidget(
|
|
items=[
|
|
WidgetListItem(id=1, title="Import #1", status="success")
|
|
],
|
|
),
|
|
icon="download",
|
|
order=10,
|
|
)
|
|
"""
|
|
|
|
key: str
|
|
widget_type: str # "list" or "breakdown"
|
|
title: str
|
|
category: str
|
|
data: WidgetData
|
|
icon: str | None = None
|
|
description: str | None = None
|
|
order: int = 100
|
|
|
|
|
|
# =============================================================================
|
|
# Protocol Interface
|
|
# =============================================================================
|
|
|
|
|
|
@runtime_checkable
|
|
class DashboardWidgetProviderProtocol(Protocol):
|
|
"""
|
|
Protocol for modules that provide dashboard widgets.
|
|
|
|
Each module implements this to expose its own widgets.
|
|
The core module's WidgetAggregator discovers and aggregates all providers.
|
|
|
|
Implementation Notes:
|
|
- Providers should be stateless (all data via db session)
|
|
- Return empty list if no widgets available (don't raise)
|
|
- Use consistent key format: "{category}.{widget_name}"
|
|
- Include icon hints for UI rendering
|
|
- Be mindful of query performance (use efficient queries)
|
|
- Translations are handled by the module itself
|
|
|
|
Example Implementation:
|
|
class MarketplaceWidgetProvider:
|
|
@property
|
|
def widgets_category(self) -> str:
|
|
return "marketplace"
|
|
|
|
def get_store_widgets(
|
|
self, db: Session, store_id: int, context: WidgetContext | None = None
|
|
) -> list[DashboardWidget]:
|
|
from app.modules.marketplace.models import MarketplaceImportJob
|
|
limit = context.limit if context else 5
|
|
jobs = db.query(MarketplaceImportJob).filter(...).limit(limit).all()
|
|
return [
|
|
DashboardWidget(
|
|
key="marketplace.recent_imports",
|
|
widget_type="list",
|
|
title="Recent Imports",
|
|
category="marketplace",
|
|
data=ListWidget(items=[...]),
|
|
icon="download"
|
|
)
|
|
]
|
|
|
|
def get_platform_widgets(
|
|
self, db: Session, platform_id: int, context: WidgetContext | None = None
|
|
) -> list[DashboardWidget]:
|
|
# Aggregate across all stores in platform
|
|
...
|
|
"""
|
|
|
|
@property
|
|
def widgets_category(self) -> str:
|
|
"""
|
|
Category name for this provider's widgets.
|
|
|
|
Should be a short, lowercase identifier matching the module's domain.
|
|
Examples: "orders", "inventory", "customers", "marketplace"
|
|
|
|
Returns:
|
|
Category string used for grouping widgets
|
|
"""
|
|
...
|
|
|
|
def get_store_widgets(
|
|
self,
|
|
db: "Session",
|
|
store_id: int,
|
|
context: WidgetContext | None = None,
|
|
) -> list[DashboardWidget]:
|
|
"""
|
|
Get widgets for a specific store dashboard.
|
|
|
|
Called by the store dashboard to display store-scoped widgets.
|
|
Should only include data belonging to the specified store.
|
|
|
|
Args:
|
|
db: Database session for queries
|
|
store_id: ID of the store to get widgets for
|
|
context: Optional filtering/scoping context
|
|
|
|
Returns:
|
|
List of DashboardWidget objects for this store
|
|
"""
|
|
...
|
|
|
|
def get_platform_widgets(
|
|
self,
|
|
db: "Session",
|
|
platform_id: int,
|
|
context: WidgetContext | None = None,
|
|
) -> list[DashboardWidget]:
|
|
"""
|
|
Get widgets aggregated for a platform.
|
|
|
|
Called by the admin dashboard to display platform-wide widgets.
|
|
Should aggregate data across all stores in the platform.
|
|
|
|
Args:
|
|
db: Database session for queries
|
|
platform_id: ID of the platform to get widgets for
|
|
context: Optional filtering/scoping context
|
|
|
|
Returns:
|
|
List of DashboardWidget objects aggregated for the platform
|
|
"""
|
|
...
|
|
|
|
def get_storefront_dashboard_cards(
|
|
self,
|
|
db: "Session",
|
|
store_id: int,
|
|
customer_id: int,
|
|
context: WidgetContext | None = None,
|
|
) -> list["StorefrontDashboardCard"]:
|
|
"""
|
|
Get cards for the storefront customer dashboard.
|
|
|
|
Called by the customer account dashboard. Each module contributes
|
|
its own cards (e.g. orders summary, loyalty points).
|
|
|
|
Args:
|
|
db: Database session for queries
|
|
store_id: ID of the store
|
|
customer_id: ID of the logged-in customer
|
|
context: Optional filtering/scoping context
|
|
|
|
Returns:
|
|
List of StorefrontDashboardCard objects
|
|
"""
|
|
...
|
|
|
|
|
|
__all__ = [
|
|
# Context
|
|
"WidgetContext",
|
|
# Item types
|
|
"WidgetListItem",
|
|
"WidgetBreakdownItem",
|
|
# Containers
|
|
"ListWidget",
|
|
"BreakdownWidget",
|
|
"WidgetData",
|
|
# Main envelope
|
|
"DashboardWidget",
|
|
# Storefront
|
|
"StorefrontDashboardCard",
|
|
# Protocol
|
|
"DashboardWidgetProviderProtocol",
|
|
]
|