# 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", ]