From adc36246b8c40478298d81905116d81f90602ed8 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Mon, 13 Apr 2026 22:53:17 +0200 Subject: [PATCH] feat(storefront): homepage, module gating, widget protocol, i18n fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/api/deps.py | 36 + .../templates/analytics/store/analytics.html | 2 +- app/modules/cart/definition.py | 2 +- app/modules/cart/locales/de.json | 5 + app/modules/cart/locales/en.json | 97 +- app/modules/cart/locales/fr.json | 5 + app/modules/cart/locales/lb.json | 5 + app/modules/catalog/definition.py | 4 +- app/modules/catalog/locales/de.json | 8 + app/modules/catalog/locales/en.json | 8 + app/modules/catalog/locales/fr.json | 8 + app/modules/catalog/locales/lb.json | 8 + .../catalog/routes/pages/storefront.py | 3 +- app/modules/cms/locales/de.json | 10 + app/modules/cms/locales/en.json | 10 + app/modules/cms/locales/fr.json | 10 + app/modules/cms/locales/lb.json | 10 + app/modules/cms/routes/pages/storefront.py | 68 + .../cms/storefront/content-page.html | 6 +- .../cms/storefront/landing-default.html | 94 +- app/modules/contracts/widgets.py | 64 + app/modules/core/routes/pages/store.py | 19 +- .../core/services/widget_aggregator.py | 44 + app/modules/customers/definition.py | 8 +- app/modules/customers/locales/de.json | 8 + app/modules/customers/locales/en.json | 8 + app/modules/customers/locales/fr.json | 8 + app/modules/customers/locales/lb.json | 8 + .../customers/routes/pages/storefront.py | 18 +- .../customers/storefront/addresses.html | 2 +- .../customers/storefront/dashboard.html | 61 +- app/modules/loyalty/definition.py | 11 +- app/modules/loyalty/locales/de.json | 1809 ++++++++-------- app/modules/loyalty/locales/en.json | 1819 +++++++++-------- app/modules/loyalty/locales/fr.json | 1803 ++++++++-------- app/modules/loyalty/locales/lb.json | 1809 ++++++++-------- .../loyalty/services/loyalty_widgets.py | 81 + .../admin/letzshop-order-detail.html | 2 +- app/modules/messaging/definition.py | 2 +- app/modules/messaging/locales/de.json | 5 + app/modules/messaging/locales/en.json | 5 + app/modules/messaging/locales/fr.json | 5 + app/modules/messaging/locales/lb.json | 5 + app/modules/orders/definition.py | 10 +- app/modules/orders/locales/de.json | 5 + app/modules/orders/locales/en.json | 5 + app/modules/orders/locales/fr.json | 5 + app/modules/orders/locales/lb.json | 5 + app/modules/orders/services/order_widgets.py | 80 + .../orders/storefront/order-detail.html | 4 +- .../templates/prospecting/admin/capture.html | 2 +- app/templates/store/partials/sidebar.html | 10 - app/templates/storefront/base.html | 39 +- docs/development/icons-guide.md | 2 +- docs/proposals/storefront-builder-vision.md | 256 +++ main.py | 20 + scripts/seed/create_default_content_pages.py | 49 + static/shared/js/icons.js | 2 +- 58 files changed, 4691 insertions(+), 3806 deletions(-) create mode 100644 app/modules/loyalty/services/loyalty_widgets.py create mode 100644 app/modules/orders/services/order_widgets.py create mode 100644 docs/proposals/storefront-builder-vision.md diff --git a/app/api/deps.py b/app/api/deps.py index 55ebd74e..49c3c09b 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -1744,3 +1744,39 @@ def get_current_customer_optional( except Exception: # Invalid token, store mismatch, or other error return None + + +# ============================================================================= +# STOREFRONT MODULE GATING +# ============================================================================= + + +def make_storefront_module_gate(module_code: str): + """ + Create a FastAPI dependency that gates storefront routes by module enablement. + + Used by main.py at route registration time: each non-core module's storefront + router gets this dependency injected automatically. The framework already knows + which module owns each route via RouteInfo.module_code — no hardcoded path map. + + Args: + module_code: The module code to check (e.g. "catalog", "orders", "loyalty") + + Returns: + A FastAPI dependency function + """ + + async def _check_module_enabled( + request: Request, + db: Session = Depends(get_db), + ) -> None: + from app.modules.service import module_service + + platform = getattr(request.state, "platform", None) + if not platform: + return # No platform context — let other middleware handle it + + if not module_service.is_module_enabled(db, platform.id, module_code): + raise HTTPException(status_code=404, detail="Page not found") + + return _check_module_enabled diff --git a/app/modules/analytics/templates/analytics/store/analytics.html b/app/modules/analytics/templates/analytics/store/analytics.html index a317b21c..3db0e0bb 100644 --- a/app/modules/analytics/templates/analytics/store/analytics.html +++ b/app/modules/analytics/templates/analytics/store/analytics.html @@ -143,7 +143,7 @@
- +

diff --git a/app/modules/cart/definition.py b/app/modules/cart/definition.py index 50af19ed..795f3a37 100644 --- a/app/modules/cart/definition.py +++ b/app/modules/cart/definition.py @@ -68,7 +68,7 @@ cart_module = ModuleDefinition( items=[ MenuItemDefinition( id="cart", - label_key="storefront.actions.cart", + label_key="cart.storefront.actions.cart", icon="shopping-cart", route="cart", order=20, diff --git a/app/modules/cart/locales/de.json b/app/modules/cart/locales/de.json index 895c8b06..59b7c781 100644 --- a/app/modules/cart/locales/de.json +++ b/app/modules/cart/locales/de.json @@ -44,5 +44,10 @@ "view_desc": "Warenkörbe der Kunden anzeigen", "manage": "Warenkörbe verwalten", "manage_desc": "Warenkörbe der Kunden bearbeiten und verwalten" + }, + "storefront": { + "actions": { + "cart": "Warenkorb" + } } } diff --git a/app/modules/cart/locales/en.json b/app/modules/cart/locales/en.json index d77292b2..4cfcd107 100644 --- a/app/modules/cart/locales/en.json +++ b/app/modules/cart/locales/en.json @@ -1,48 +1,53 @@ { - "title": "Shopping Cart", - "description": "Shopping cart management for customers", - "cart": { - "title": "Your Cart", - "empty": "Your cart is empty", - "empty_subtitle": "Add items to start shopping", - "continue_shopping": "Continue Shopping", - "proceed_to_checkout": "Proceed to Checkout" - }, - "item": { - "product": "Product", - "quantity": "Quantity", - "price": "Price", - "total": "Total", - "remove": "Remove", - "update": "Update" - }, - "summary": { - "title": "Order Summary", - "subtotal": "Subtotal", - "shipping": "Shipping", - "estimated_shipping": "Calculated at checkout", - "tax": "Tax", - "total": "Total" - }, - "validation": { - "invalid_quantity": "Invalid quantity", - "min_quantity": "Minimum quantity is {min}", - "max_quantity": "Maximum quantity is {max}", - "insufficient_inventory": "Only {available} available" - }, - "permissions": { - "view": "View Carts", - "view_desc": "View customer shopping carts", - "manage": "Manage Carts", - "manage_desc": "Modify and manage customer carts" - }, - "messages": { - "item_added": "Item added to cart", - "item_updated": "Cart updated", - "item_removed": "Item removed from cart", - "cart_cleared": "Cart cleared", - "product_not_available": "Product not available", - "error_adding": "Error adding item to cart", - "error_updating": "Error updating cart" - } + "title": "Shopping Cart", + "description": "Shopping cart management for customers", + "cart": { + "title": "Your Cart", + "empty": "Your cart is empty", + "empty_subtitle": "Add items to start shopping", + "continue_shopping": "Continue Shopping", + "proceed_to_checkout": "Proceed to Checkout" + }, + "item": { + "product": "Product", + "quantity": "Quantity", + "price": "Price", + "total": "Total", + "remove": "Remove", + "update": "Update" + }, + "summary": { + "title": "Order Summary", + "subtotal": "Subtotal", + "shipping": "Shipping", + "estimated_shipping": "Calculated at checkout", + "tax": "Tax", + "total": "Total" + }, + "validation": { + "invalid_quantity": "Invalid quantity", + "min_quantity": "Minimum quantity is {min}", + "max_quantity": "Maximum quantity is {max}", + "insufficient_inventory": "Only {available} available" + }, + "permissions": { + "view": "View Carts", + "view_desc": "View customer shopping carts", + "manage": "Manage Carts", + "manage_desc": "Modify and manage customer carts" + }, + "messages": { + "item_added": "Item added to cart", + "item_updated": "Cart updated", + "item_removed": "Item removed from cart", + "cart_cleared": "Cart cleared", + "product_not_available": "Product not available", + "error_adding": "Error adding item to cart", + "error_updating": "Error updating cart" + }, + "storefront": { + "actions": { + "cart": "Cart" + } + } } diff --git a/app/modules/cart/locales/fr.json b/app/modules/cart/locales/fr.json index 682eae58..dea93df2 100644 --- a/app/modules/cart/locales/fr.json +++ b/app/modules/cart/locales/fr.json @@ -44,5 +44,10 @@ "view_desc": "Voir les paniers des clients", "manage": "Gérer les paniers", "manage_desc": "Modifier et gérer les paniers des clients" + }, + "storefront": { + "actions": { + "cart": "Panier" + } } } diff --git a/app/modules/cart/locales/lb.json b/app/modules/cart/locales/lb.json index c9fa3d81..c04ceb33 100644 --- a/app/modules/cart/locales/lb.json +++ b/app/modules/cart/locales/lb.json @@ -44,5 +44,10 @@ "view_desc": "Clientekuerf kucken", "manage": "Kuerf verwalten", "manage_desc": "Clientekuerf änneren a verwalten" + }, + "storefront": { + "actions": { + "cart": "Kuerf" + } } } diff --git a/app/modules/catalog/definition.py b/app/modules/catalog/definition.py index 7a1d1925..77f23529 100644 --- a/app/modules/catalog/definition.py +++ b/app/modules/catalog/definition.py @@ -134,7 +134,7 @@ catalog_module = ModuleDefinition( items=[ MenuItemDefinition( id="products", - label_key="storefront.nav.products", + label_key="catalog.storefront.nav.products", icon="shopping-bag", route="products", order=10, @@ -148,7 +148,7 @@ catalog_module = ModuleDefinition( items=[ MenuItemDefinition( id="search", - label_key="storefront.actions.search", + label_key="catalog.storefront.actions.search", icon="search", route="", order=10, diff --git a/app/modules/catalog/locales/de.json b/app/modules/catalog/locales/de.json index fa287749..94d8a9d7 100644 --- a/app/modules/catalog/locales/de.json +++ b/app/modules/catalog/locales/de.json @@ -89,5 +89,13 @@ "products_import_desc": "Massenimport von Produkten", "products_export": "Produkte exportieren", "products_export_desc": "Produktdaten exportieren" + }, + "storefront": { + "nav": { + "products": "Produkte" + }, + "actions": { + "search": "Suchen" + } } } diff --git a/app/modules/catalog/locales/en.json b/app/modules/catalog/locales/en.json index 8aa11642..64959966 100644 --- a/app/modules/catalog/locales/en.json +++ b/app/modules/catalog/locales/en.json @@ -107,5 +107,13 @@ "menu": { "products_inventory": "Products & Inventory", "all_products": "All Products" + }, + "storefront": { + "nav": { + "products": "Products" + }, + "actions": { + "search": "Search" + } } } diff --git a/app/modules/catalog/locales/fr.json b/app/modules/catalog/locales/fr.json index 22c49d18..0265b78e 100644 --- a/app/modules/catalog/locales/fr.json +++ b/app/modules/catalog/locales/fr.json @@ -89,5 +89,13 @@ "products_import_desc": "Importation en masse de produits", "products_export": "Exporter les produits", "products_export_desc": "Exporter les données produits" + }, + "storefront": { + "nav": { + "products": "Produits" + }, + "actions": { + "search": "Rechercher" + } } } diff --git a/app/modules/catalog/locales/lb.json b/app/modules/catalog/locales/lb.json index faf1cbd9..ab9f7498 100644 --- a/app/modules/catalog/locales/lb.json +++ b/app/modules/catalog/locales/lb.json @@ -89,5 +89,13 @@ "products_import_desc": "Massenimport vu Produiten", "products_export": "Produiten exportéieren", "products_export_desc": "Produitdaten exportéieren" + }, + "storefront": { + "nav": { + "products": "Produkter" + }, + "actions": { + "search": "Sichen" + } } } diff --git a/app/modules/catalog/routes/pages/storefront.py b/app/modules/catalog/routes/pages/storefront.py index f3ca83e4..af3f5575 100644 --- a/app/modules/catalog/routes/pages/storefront.py +++ b/app/modules/catalog/routes/pages/storefront.py @@ -30,11 +30,10 @@ router = APIRouter() # ============================================================================ -@router.get("/", response_class=HTMLResponse, include_in_schema=False) @router.get("/products", response_class=HTMLResponse, include_in_schema=False) async def shop_products_page(request: Request, db: Session = Depends(get_db)): """ - Render shop homepage / product catalog. + Render product catalog listing. Shows featured products and categories. """ logger.debug( diff --git a/app/modules/cms/locales/de.json b/app/modules/cms/locales/de.json index 1aff18f4..06d57fb5 100644 --- a/app/modules/cms/locales/de.json +++ b/app/modules/cms/locales/de.json @@ -388,5 +388,15 @@ }, "confirmations": { "delete_file": "Sind Sie sicher, dass Sie diese Datei löschen möchten? Dies kann nicht rückgängig gemacht werden." + }, + "storefront": { + "my_account": "Mein Konto", + "learn_more": "Mehr erfahren", + "explore": "Entdecken", + "quick_links": "Schnellzugriff", + "information": "Informationen", + "about": "Über uns", + "contact": "Kontakt", + "faq": "FAQ" } } diff --git a/app/modules/cms/locales/en.json b/app/modules/cms/locales/en.json index bec2ef20..8b3f74fe 100644 --- a/app/modules/cms/locales/en.json +++ b/app/modules/cms/locales/en.json @@ -388,5 +388,15 @@ "content_pages": "Content Pages", "store_themes": "Store Themes", "media_library": "Media Library" + }, + "storefront": { + "my_account": "My Account", + "learn_more": "Learn More", + "explore": "Explore", + "quick_links": "Quick Links", + "information": "Information", + "about": "About Us", + "contact": "Contact", + "faq": "FAQ" } } diff --git a/app/modules/cms/locales/fr.json b/app/modules/cms/locales/fr.json index affe89ad..d8fbd807 100644 --- a/app/modules/cms/locales/fr.json +++ b/app/modules/cms/locales/fr.json @@ -388,5 +388,15 @@ }, "confirmations": { "delete_file": "Êtes-vous sûr de vouloir supprimer ce fichier ? Cette action est irréversible." + }, + "storefront": { + "my_account": "Mon Compte", + "learn_more": "En savoir plus", + "explore": "Découvrir", + "quick_links": "Liens rapides", + "information": "Informations", + "about": "À propos", + "contact": "Contact", + "faq": "FAQ" } } diff --git a/app/modules/cms/locales/lb.json b/app/modules/cms/locales/lb.json index b9d97d8e..483ef6bc 100644 --- a/app/modules/cms/locales/lb.json +++ b/app/modules/cms/locales/lb.json @@ -388,5 +388,15 @@ }, "confirmations": { "delete_file": "Sidd Dir sécher datt Dir dëse Fichier läsche wëllt? Dat kann net réckgängeg gemaach ginn." + }, + "storefront": { + "my_account": "Mäi Kont", + "learn_more": "Méi gewuer ginn", + "explore": "Entdecken", + "quick_links": "Schnellzougrëff", + "information": "Informatiounen", + "about": "Iwwer eis", + "contact": "Kontakt", + "faq": "FAQ" } } diff --git a/app/modules/cms/routes/pages/storefront.py b/app/modules/cms/routes/pages/storefront.py index 3561b74a..fd8c734e 100644 --- a/app/modules/cms/routes/pages/storefront.py +++ b/app/modules/cms/routes/pages/storefront.py @@ -28,6 +28,71 @@ ROUTE_CONFIG = { } +# ============================================================================ +# STOREFRONT HOMEPAGE +# ============================================================================ + + +@router.get("/", response_class=HTMLResponse, include_in_schema=False) +async def storefront_homepage( + request: Request, + db: Session = Depends(get_db), +): + """ + Storefront homepage handler. + + Looks for a CMS page with slug="home" (store override → store default), + and renders the appropriate landing template. Falls back to the default + landing template when no CMS homepage exists. + """ + store = getattr(request.state, "store", None) + platform = getattr(request.state, "platform", None) + store_id = store.id if store else None + if not platform: + raise HTTPException(status_code=400, detail="Platform context required") + + # Try to load a homepage from CMS (store override → store default) + page = content_page_service.get_page_for_store( + db, + platform_id=platform.id, + slug="home", + store_id=store_id, + include_unpublished=False, + ) + + # Resolve placeholders for store default pages (title + content) + page_content = None + page_title = None + if page: + page_content = page.content + page_title = page.title + if page.is_store_default and store: + page_content = content_page_service.resolve_placeholders( + page.content, store + ) + page_title = content_page_service.resolve_placeholders( + page.title, store + ) + + context = get_storefront_context(request, db=db, page=page) + if page_content: + context["page_content"] = page_content + if page_title: + context["page_title"] = page_title + + # Select template based on page.template field (or default) + template_map = { + "full": "cms/storefront/landing-full.html", + "modern": "cms/storefront/landing-modern.html", + "minimal": "cms/storefront/landing-minimal.html", + } + template_name = "cms/storefront/landing-default.html" + if page and page.template: + template_name = template_map.get(page.template, template_name) + + return templates.TemplateResponse(template_name, context) + + # ============================================================================ # DYNAMIC CONTENT PAGES (CMS) # ============================================================================ @@ -103,10 +168,13 @@ async def generic_content_page( # Resolve placeholders in store default pages ({{store_name}}, etc.) page_content = page.content + page_title = page.title if page.is_store_default and store: page_content = content_page_service.resolve_placeholders(page.content, store) + page_title = content_page_service.resolve_placeholders(page.title, store) context = get_storefront_context(request, db=db, page=page) + context["page_title"] = page_title context["page_content"] = page_content # Select template based on page.template field diff --git a/app/modules/cms/templates/cms/storefront/content-page.html b/app/modules/cms/templates/cms/storefront/content-page.html index 45a99664..73bfb534 100644 --- a/app/modules/cms/templates/cms/storefront/content-page.html +++ b/app/modules/cms/templates/cms/storefront/content-page.html @@ -3,7 +3,7 @@ {% extends "storefront/base.html" %} {# Dynamic title from CMS #} -{% block title %}{{ page.title }}{% endblock %} +{% block title %}{{ page_title or page.title }}{% endblock %} {# SEO from CMS #} {% block meta_description %}{{ page.meta_description or page.title }}{% endblock %} @@ -16,13 +16,13 @@ {# Page Header #}

- {{ page.title }} + {{ page_title or page.title }}

{# Optional: Show store override badge for debugging #} diff --git a/app/modules/cms/templates/cms/storefront/landing-default.html b/app/modules/cms/templates/cms/storefront/landing-default.html index 58db4604..9ba28e44 100644 --- a/app/modules/cms/templates/cms/storefront/landing-default.html +++ b/app/modules/cms/templates/cms/storefront/landing-default.html @@ -1,10 +1,9 @@ -{# app/templates/store/landing-default.html #} -{# standalone #} +{# app/modules/cms/templates/cms/storefront/landing-default.html #} {# Default/Minimal Landing Page Template #} {% extends "storefront/base.html" %} {% block title %}{{ store.name }}{% endblock %} -{% block meta_description %}{{ page.meta_description or store.description or store.name }}{% endblock %} +{% block meta_description %}{{ page.meta_description or store.description or store.name if page else store.description or store.name }}{% endblock %} {% block content %}
@@ -24,7 +23,7 @@ {# Title #}

- {{ page.title or store.name }} + {{ page_title or store.name }}

{# Tagline #} @@ -34,18 +33,31 @@

{% endif %} - {# CTA Button #} + {# CTA Buttons — driven by storefront_nav (module-agnostic) #} + {% set nav_items = storefront_nav.get('nav', []) %}
- - Browse Our Shop + {{ _(nav_items[0].label_key) }} - {% if page.content %} + {% else %} + {# Fallback: account link when no module nav items #} + + {{ _('cms.storefront.my_account') }} + + + {% endif %} + + {% if page and page.content %} - Learn More + {{ _('cms.storefront.learn_more') }} {% endif %}
@@ -54,73 +66,65 @@ {# Content Section (if provided) #} - {% if page.content %} + {% if page_content %}
- {{ page.content | safe }}{# sanitized: CMS content #} + {{ page_content | safe }}{# sanitized: CMS content #}
{% endif %} - {# Quick Links Section #} + {# Quick Links Section — driven by nav items and CMS pages #} + {% set account_items = storefront_nav.get('account', []) %} + {% set all_links = nav_items + account_items %} + {% if all_links or header_pages %}

- Explore + {{ _('cms.storefront.explore') }}

- -
🛍️
+
+ +

- Shop Products + {{ _(item.label_key) }}

-

- Browse our complete catalog -

+ {% endfor %} - {% if header_pages %} - {% for page in header_pages[:2] %} + {# Fill remaining slots with CMS header pages #} + {% set remaining = 3 - all_links[:3]|length %} + {% if remaining > 0 and header_pages %} + {% for page in header_pages[:remaining] %} -
📄
+
+ +

{{ page.title }}

+ {% if page.meta_description %}

- {{ page.meta_description or 'Learn more' }} + {{ page.meta_description }}

+ {% endif %}
{% endfor %} - {% else %} - -
ℹ️
-

- About Us -

-

- Learn about our story -

-
- - -
📧
-

- Contact -

-

- Get in touch with us -

-
{% endif %}
+ {% endif %}
{% endblock %} diff --git a/app/modules/contracts/widgets.py b/app/modules/contracts/widgets.py index 57e87318..2fc24bc9 100644 --- a/app/modules/contracts/widgets.py +++ b/app/modules/contracts/widgets.py @@ -80,6 +80,44 @@ class WidgetContext: 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 # ============================================================================= @@ -330,6 +368,30 @@ class DashboardWidgetProviderProtocol(Protocol): """ ... + 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 @@ -343,6 +405,8 @@ __all__ = [ "WidgetData", # Main envelope "DashboardWidget", + # Storefront + "StorefrontDashboardCard", # Protocol "DashboardWidgetProviderProtocol", ] diff --git a/app/modules/core/routes/pages/store.py b/app/modules/core/routes/pages/store.py index a7e3011a..eb6e04b3 100644 --- a/app/modules/core/routes/pages/store.py +++ b/app/modules/core/routes/pages/store.py @@ -9,11 +9,13 @@ Store pages for core functionality: """ from fastapi import APIRouter, Depends, Request -from fastapi.responses import HTMLResponse +from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session from app.api.deps import ( + UserContext, get_current_store_from_cookie_or_header, + get_current_store_optional, get_db, get_resolved_store_code, ) @@ -24,6 +26,21 @@ from app.templates_config import templates router = APIRouter() +# ============================================================================ +# STORE ROOT REDIRECT +# ============================================================================ + + +@router.get("/", response_class=RedirectResponse, include_in_schema=False) +async def store_root( + current_user: UserContext | None = Depends(get_current_store_optional), +): + """Redirect /store/ based on authentication status.""" + if current_user: + return RedirectResponse(url="/store/dashboard", status_code=302) + return RedirectResponse(url="/store/login", status_code=302) + + # ============================================================================ # STORE DASHBOARD # ============================================================================ diff --git a/app/modules/core/services/widget_aggregator.py b/app/modules/core/services/widget_aggregator.py index 0035dcf1..9f8b6fc9 100644 --- a/app/modules/core/services/widget_aggregator.py +++ b/app/modules/core/services/widget_aggregator.py @@ -34,6 +34,7 @@ from sqlalchemy.orm import Session from app.modules.contracts.widgets import ( DashboardWidget, DashboardWidgetProviderProtocol, + StorefrontDashboardCard, WidgetContext, ) @@ -233,6 +234,49 @@ class WidgetAggregatorService: return widget return None + def get_storefront_dashboard_cards( + self, + db: Session, + store_id: int, + customer_id: int, + platform_id: int, + context: WidgetContext | None = None, + ) -> list[StorefrontDashboardCard]: + """ + Get dashboard cards for the storefront customer account page. + + Collects cards from all enabled modules that implement + get_storefront_dashboard_cards(), sorted by order. + + Args: + db: Database session + store_id: ID of the store + customer_id: ID of the logged-in customer + platform_id: Platform ID (for module enablement check) + context: Optional filtering/scoping context + + Returns: + Flat list of StorefrontDashboardCard sorted by order + """ + providers = self._get_enabled_providers(db, platform_id) + cards: list[StorefrontDashboardCard] = [] + + for module, provider in providers: + if not hasattr(provider, "get_storefront_dashboard_cards"): + continue + try: + module_cards = provider.get_storefront_dashboard_cards( + db, store_id, customer_id, context + ) + if module_cards: + cards.extend(module_cards) + except Exception as e: + logger.warning( + f"Failed to get storefront cards from module {module.code}: {e}" + ) + + return sorted(cards, key=lambda c: c.order) + def get_available_categories( self, db: Session, platform_id: int ) -> list[str]: diff --git a/app/modules/customers/definition.py b/app/modules/customers/definition.py index 9e4261f0..6c2b7bf5 100644 --- a/app/modules/customers/definition.py +++ b/app/modules/customers/definition.py @@ -141,28 +141,28 @@ customers_module = ModuleDefinition( items=[ MenuItemDefinition( id="dashboard", - label_key="storefront.account.dashboard", + label_key="customers.storefront.account.dashboard", icon="home", route="account/dashboard", order=10, ), MenuItemDefinition( id="profile", - label_key="storefront.account.profile", + label_key="customers.storefront.account.profile", icon="user", route="account/profile", order=20, ), MenuItemDefinition( id="addresses", - label_key="storefront.account.addresses", + label_key="customers.storefront.account.addresses", icon="map-pin", route="account/addresses", order=30, ), MenuItemDefinition( id="settings", - label_key="storefront.account.settings", + label_key="customers.storefront.account.settings", icon="cog", route="account/settings", order=90, diff --git a/app/modules/customers/locales/de.json b/app/modules/customers/locales/de.json index 95a3733d..10739522 100644 --- a/app/modules/customers/locales/de.json +++ b/app/modules/customers/locales/de.json @@ -52,5 +52,13 @@ "customers_delete_desc": "Kundendatensätze entfernen", "customers_export": "Kunden exportieren", "customers_export_desc": "Kundendaten exportieren" + }, + "storefront": { + "account": { + "dashboard": "Dashboard", + "profile": "Profil", + "addresses": "Adressen", + "settings": "Einstellungen" + } } } diff --git a/app/modules/customers/locales/en.json b/app/modules/customers/locales/en.json index 76f7fa49..b91b68e4 100644 --- a/app/modules/customers/locales/en.json +++ b/app/modules/customers/locales/en.json @@ -52,5 +52,13 @@ "customers_section": "Customers", "customers": "Customers", "all_customers": "All Customers" + }, + "storefront": { + "account": { + "dashboard": "Dashboard", + "profile": "Profile", + "addresses": "Addresses", + "settings": "Settings" + } } } diff --git a/app/modules/customers/locales/fr.json b/app/modules/customers/locales/fr.json index 93f11095..baee97f1 100644 --- a/app/modules/customers/locales/fr.json +++ b/app/modules/customers/locales/fr.json @@ -52,5 +52,13 @@ "customers_delete_desc": "Supprimer les fiches clients", "customers_export": "Exporter les clients", "customers_export_desc": "Exporter les données clients" + }, + "storefront": { + "account": { + "dashboard": "Tableau de bord", + "profile": "Profil", + "addresses": "Adresses", + "settings": "Paramètres" + } } } diff --git a/app/modules/customers/locales/lb.json b/app/modules/customers/locales/lb.json index 83350438..e31667d4 100644 --- a/app/modules/customers/locales/lb.json +++ b/app/modules/customers/locales/lb.json @@ -52,5 +52,13 @@ "customers_delete_desc": "Clientedossieren ewechhuelen", "customers_export": "Clienten exportéieren", "customers_export_desc": "Clientedaten exportéieren" + }, + "storefront": { + "account": { + "dashboard": "Dashboard", + "profile": "Profil", + "addresses": "Adressen", + "settings": "Astellungen" + } } } diff --git a/app/modules/customers/routes/pages/storefront.py b/app/modules/customers/routes/pages/storefront.py index 9a2bf34e..a731f969 100644 --- a/app/modules/customers/routes/pages/storefront.py +++ b/app/modules/customers/routes/pages/storefront.py @@ -195,9 +195,25 @@ async def shop_account_dashboard_page( }, ) + # Collect dashboard cards from enabled modules via widget protocol + from app.modules.core.services.widget_aggregator import widget_aggregator + + store = getattr(request.state, "store", None) + platform = getattr(request.state, "platform", None) + dashboard_cards = [] + if store and platform: + dashboard_cards = widget_aggregator.get_storefront_dashboard_cards( + db, + store_id=store.id, + customer_id=current_customer.id, + platform_id=platform.id, + ) + return templates.TemplateResponse( "customers/storefront/dashboard.html", - get_storefront_context(request, db=db, user=current_customer), + get_storefront_context( + request, db=db, user=current_customer, dashboard_cards=dashboard_cards + ), ) diff --git a/app/modules/customers/templates/customers/storefront/addresses.html b/app/modules/customers/templates/customers/storefront/addresses.html index 06409fab..9c8c85c1 100644 --- a/app/modules/customers/templates/customers/storefront/addresses.html +++ b/app/modules/customers/templates/customers/storefront/addresses.html @@ -37,7 +37,7 @@
- +

No addresses yet

Add your first address to speed up checkout.

diff --git a/app/templates/store/partials/sidebar.html b/app/templates/store/partials/sidebar.html index edf7a1f1..0060b15f 100644 --- a/app/templates/store/partials/sidebar.html +++ b/app/templates/store/partials/sidebar.html @@ -106,16 +106,6 @@
- {% if 'catalog' in enabled_modules %} - -
- -
- {% endif %}
{% endmacro %} diff --git a/app/templates/storefront/base.html b/app/templates/storefront/base.html index 339f5179..6d98ed52 100644 --- a/app/templates/storefront/base.html +++ b/app/templates/storefront/base.html @@ -100,7 +100,7 @@ {# CMS pages (About, Contact) are already dynamic via header_pages #} {% for page in header_pages|default([]) %} - {{ page.title }} + {{ page.get_translated_title(current_language) }} {% endfor %} @@ -108,7 +108,10 @@ {# Right side actions #}
- {% if 'catalog' in enabled_modules|default([]) %} + {# Action items from enabled modules (search, cart, etc.) #} + {% set action_ids = storefront_nav.get('actions', [])|map(attribute='id')|list %} + + {% if 'search' in action_ids %} {# Search #} {% endif %} - {% if 'cart' in enabled_modules|default([]) %} + {% if 'cart' in action_ids %} {# Cart #} @@ -131,7 +134,7 @@ style="background-color: var(--color-accent)"> - {% endif %} + {% endif %}{# cart #} {# Theme toggle #}