diff --git a/.architecture-rules/module.yaml b/.architecture-rules/module.yaml index 266efcf2..6e418fe9 100644 --- a/.architecture-rules/module.yaml +++ b/.architecture-rules/module.yaml @@ -536,7 +536,8 @@ module_rules: EXCEPTIONS (allowed in legacy): - __init__.py (re-exports for backwards compatibility) - - auth.py (core authentication schemas) + - base.py (base schema classes - infrastructure) + - auth.py (core authentication schemas - cross-cutting) - Files with # noqa: mod-019 comment WHY THIS MATTERS: @@ -548,4 +549,5 @@ module_rules: - "models/schema/*.py" exceptions: - "__init__.py" + - "base.py" - "auth.py" diff --git a/alembic/env.py b/alembic/env.py index ba3a8782..d05f46f4 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -40,7 +40,7 @@ print("=" * 70) # ADMIN MODELS # ---------------------------------------------------------------------------- try: - from models.database.admin import ( + from app.modules.tenancy.models import ( AdminAuditLog, AdminNotification, AdminSession, @@ -61,7 +61,7 @@ except ImportError as e: # USER MODEL # ---------------------------------------------------------------------------- try: - from models.database.user import User + from app.modules.tenancy.models import User print(" ✓ User model imported") except ImportError as e: @@ -71,7 +71,7 @@ except ImportError as e: # VENDOR MODELS # ---------------------------------------------------------------------------- try: - from models.database.vendor import Role, Vendor, VendorUser + from app.modules.tenancy.models import Role, Vendor, VendorUser print(" ✓ Vendor models imported (3 models)") print(" - Vendor") @@ -81,14 +81,14 @@ except ImportError as e: print(f" ✗ Vendor models failed: {e}") try: - from models.database.vendor_domain import VendorDomain + from app.modules.tenancy.models import VendorDomain print(" ✓ VendorDomain model imported") except ImportError as e: print(f" ✗ VendorDomain model failed: {e}") try: - from models.database.vendor_theme import VendorTheme + from app.modules.cms.models import VendorTheme print(" ✓ VendorTheme model imported") except ImportError as e: diff --git a/app/api/deps.py b/app/api/deps.py index ce273dce..18adcb86 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -51,8 +51,8 @@ from app.modules.tenancy.exceptions import ( from app.modules.tenancy.services.vendor_service import vendor_service from middleware.auth import AuthManager from middleware.rate_limiter import RateLimiter -from models.database.user import User as UserModel -from models.database.vendor import Vendor +from app.modules.tenancy.models import User as UserModel +from app.modules.tenancy.models import Vendor from models.schema.auth import UserContext # Initialize dependencies @@ -381,7 +381,7 @@ def get_admin_with_platform_context( InvalidTokenException: If platform admin token missing platform info InsufficientPermissionsException: If platform access revoked """ - from models.database.platform import Platform + from app.modules.tenancy.models import Platform # Get raw token for platform_id extraction token, source = _get_token_from_request( @@ -553,7 +553,7 @@ def require_menu_access(menu_item_id: str, frontend_type: "FrontendType"): from app.modules.registry import get_menu_item_module from app.modules.service import module_service from app.modules.core.services.menu_service import menu_service - from models.database.admin_menu_config import FrontendType as FT + from app.modules.enums import FrontendType as FT def _check_menu_access( request: Request, diff --git a/app/api/v1/admin/__init__.py b/app/api/v1/admin/__init__.py index 98b7a19d..e15cf15a 100644 --- a/app/api/v1/admin/__init__.py +++ b/app/api/v1/admin/__init__.py @@ -2,16 +2,11 @@ """ Admin API router aggregation. -This module combines legacy admin routes with auto-discovered module routes. +This module combines auto-discovered module routes for the admin API. -LEGACY ROUTES (defined in app/api/v1/admin/): -- /menu-config/* - Navigation configuration (super admin) -- /modules/* - Module management (super admin) -- /module-config/* - Module settings (super admin) - -AUTO-DISCOVERED MODULE ROUTES: -- tenancy: auth, admin_users, users, companies, platforms, vendors, vendor_domains -- core: dashboard, settings +All admin routes are now auto-discovered from modules: +- tenancy: auth, admin_users, users, companies, platforms, vendors, vendor_domains, modules, module_config +- core: dashboard, settings, menu_config - messaging: messages, notifications, email-templates - monitoring: logs, tasks, tests, code_quality, audit, platform-health - billing: subscriptions, invoices, payments @@ -30,44 +25,18 @@ IMPORTANT: from fastapi import APIRouter -# Import all admin routers (legacy routes that haven't been migrated to modules) -# NOTE: Migrated to modules (auto-discovered): -# - tenancy: auth, admin_users, users, companies, platforms, vendors, vendor_domains -# - core: dashboard, settings -# - messaging: messages, notifications, email_templates -# - monitoring: logs, tasks, tests, code_quality, audit, platform_health -# - cms: content_pages, images, media, vendor_themes -from . import ( - menu_config, - module_config, - modules, -) # Create admin router router = APIRouter() -# ============================================================================ -# Framework Config (remain in legacy - super admin only) -# ============================================================================ - -# Include menu configuration endpoints (super admin only) -router.include_router(menu_config.router, tags=["admin-menu-config"]) - -# Include module management endpoints (super admin only) -router.include_router(modules.router, tags=["admin-modules"]) - -# Include module configuration endpoints (super admin only) -router.include_router(module_config.router, tags=["admin-module-config"]) - - # ============================================================================ # Auto-discovered Module Routes # ============================================================================ -# Routes from self-contained modules are auto-discovered and registered. -# Modules include: billing, inventory, orders, marketplace, cms, customers, -# monitoring (logs, tasks, tests, code_quality, audit, platform_health), -# messaging (messages, notifications, email_templates) +# All routes from self-contained modules are auto-discovered and registered. +# Legacy routes have been migrated to their respective modules: +# - menu_config -> core module +# - modules, module_config -> tenancy module from app.modules.routes import get_admin_api_routes diff --git a/app/config/menu_registry.py b/app/config/menu_registry.py index 386e6eab..fcb1359f 100644 --- a/app/config/menu_registry.py +++ b/app/config/menu_registry.py @@ -14,7 +14,7 @@ Database only stores visibility overrides (is_visible=False). from enum import Enum -from models.database.admin_menu_config import FrontendType +from app.modules.enums import FrontendType class AdminMenuItem(str, Enum): diff --git a/app/core/feature_gate.py b/app/core/feature_gate.py index ee27554d..7f057bad 100644 --- a/app/core/feature_gate.py +++ b/app/core/feature_gate.py @@ -39,7 +39,7 @@ from app.api.deps import get_current_vendor_api from app.core.database import get_db from app.modules.billing.services.feature_service import feature_service from app.modules.billing.models import FeatureCode -from models.database.user import User +from app.modules.tenancy.models import User logger = logging.getLogger(__name__) diff --git a/app/core/logging.py b/app/core/logging.py index 4bc2f7d4..627ee62c 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -33,7 +33,7 @@ class DatabaseLogHandler(logging.Handler): """Emit a log record to the database.""" try: from app.core.database import SessionLocal - from models.database.admin import ApplicationLog + from app.modules.tenancy.models import ApplicationLog # Skip if no database session available db = SessionLocal() diff --git a/app/core/theme_presets.py b/app/core/theme_presets.py index 20eea4a4..827aea53 100644 --- a/app/core/theme_presets.py +++ b/app/core/theme_presets.py @@ -6,7 +6,7 @@ Presets define default color schemes, fonts, and layouts that vendors can choose Each preset provides a complete theme configuration that can be customized further. """ -from models.database.vendor_theme import VendorTheme +from app.modules.cms.models import VendorTheme THEME_PRESETS = { "default": { diff --git a/app/modules/analytics/definition.py b/app/modules/analytics/definition.py index 2dc71d68..3f2cd10c 100644 --- a/app/modules/analytics/definition.py +++ b/app/modules/analytics/definition.py @@ -6,8 +6,8 @@ Defines the analytics module including its features, menu items, route configurations, and self-contained module settings. """ -from app.modules.base import ModuleDefinition -from models.database.admin_menu_config import FrontendType +from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition +from app.modules.enums import FrontendType def _get_vendor_api_router(): @@ -45,6 +45,27 @@ analytics_module = ModuleDefinition( "analytics", # Vendor analytics page ], }, + # New module-driven menu definitions + menus={ + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="main", + label_key=None, + icon=None, + order=0, + is_collapsible=False, + items=[ + MenuItemDefinition( + id="analytics", + label_key="analytics.menu.analytics", + icon="chart-bar", + route="/vendor/{vendor_code}/analytics", + order=20, + ), + ], + ), + ], + }, is_core=False, # ========================================================================= # Self-Contained Module Configuration diff --git a/app/modules/analytics/routes/api/vendor.py b/app/modules/analytics/routes/api/vendor.py index 5fa88131..d7268514 100644 --- a/app/modules/analytics/routes/api/vendor.py +++ b/app/modules/analytics/routes/api/vendor.py @@ -25,7 +25,7 @@ from app.modules.analytics.schemas import ( VendorAnalyticsResponse, ) from app.modules.billing.models import FeatureCode -from models.database.user import User +from app.modules.tenancy.models import User router = APIRouter( prefix="/analytics", diff --git a/app/modules/analytics/routes/pages/admin.py b/app/modules/analytics/routes/pages/admin.py index 5b1ee752..3841d469 100644 --- a/app/modules/analytics/routes/pages/admin.py +++ b/app/modules/analytics/routes/pages/admin.py @@ -15,8 +15,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context from app.templates_config import templates -from models.database.admin_menu_config import FrontendType -from models.database.user import User +from app.modules.enums import FrontendType +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/analytics/routes/pages/vendor.py b/app/modules/analytics/routes/pages/vendor.py index 7b59d7d0..5f3aca56 100644 --- a/app/modules/analytics/routes/pages/vendor.py +++ b/app/modules/analytics/routes/pages/vendor.py @@ -14,8 +14,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_from_cookie_or_header, get_db from app.modules.core.services.platform_settings_service import platform_settings_service # noqa: MOD-004 - shared platform service from app.templates_config import templates -from models.database.user import User -from models.database.vendor import Vendor +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/analytics/services/stats_service.py b/app/modules/analytics/services/stats_service.py index 356f023d..0bb786b8 100644 --- a/app/modules/analytics/services/stats_service.py +++ b/app/modules/analytics/services/stats_service.py @@ -24,8 +24,8 @@ from app.modules.inventory.models import Inventory from app.modules.marketplace.models import MarketplaceImportJob, MarketplaceProduct from app.modules.orders.models import Order from app.modules.catalog.models import Product -from models.database.user import User -from models.database.vendor import Vendor +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/analytics/services/usage_service.py b/app/modules/analytics/services/usage_service.py index 6c75253d..e294b294 100644 --- a/app/modules/analytics/services/usage_service.py +++ b/app/modules/analytics/services/usage_service.py @@ -18,7 +18,7 @@ from sqlalchemy.orm import Session from app.modules.catalog.models import Product from app.modules.billing.models import SubscriptionTier, VendorSubscription -from models.database.vendor import VendorUser +from app.modules.tenancy.models import VendorUser logger = logging.getLogger(__name__) diff --git a/app/modules/base.py b/app/modules/base.py index 711c375a..73ce69ce 100644 --- a/app/modules/base.py +++ b/app/modules/base.py @@ -47,6 +47,93 @@ if TYPE_CHECKING: from app.modules.enums import FrontendType +# ============================================================================= +# Menu Item Definitions +# ============================================================================= + + +@dataclass +class MenuItemDefinition: + """ + Definition of a single menu item within a section. + + Attributes: + id: Unique identifier (e.g., "catalog.products", "orders.list") + label_key: i18n key for the menu item label + icon: Lucide icon name (e.g., "box", "shopping-cart") + route: URL path (can include placeholders like {vendor_code}) + order: Sort order within section (lower = higher priority) + is_mandatory: If True, cannot be hidden by user preferences + requires_permission: Permission code required to see this item + badge_source: Key for dynamic badge count (e.g., "pending_orders_count") + is_super_admin_only: Only visible to super admins (admin frontend only) + + Example: + MenuItemDefinition( + id="catalog.products", + label_key="catalog.menu.products", + icon="box", + route="/admin/catalog/products", + order=10, + is_mandatory=True + ) + """ + + id: str + label_key: str + icon: str + route: str + order: int = 100 + is_mandatory: bool = False + requires_permission: str | None = None + badge_source: str | None = None + is_super_admin_only: bool = False + + +@dataclass +class MenuSectionDefinition: + """ + Definition of a menu section containing related menu items. + + Sections group related menu items together in the sidebar. + A section can be collapsed/expanded by the user. + + Attributes: + id: Unique section identifier (e.g., "catalog", "orders") + label_key: i18n key for section header (None for headerless sections) + icon: Lucide icon name for section (optional) + order: Sort order among sections (lower = higher priority) + items: List of menu items in this section + is_super_admin_only: Only visible to super admins + is_collapsible: Whether section can be collapsed + + Example: + MenuSectionDefinition( + id="catalog", + label_key="catalog.menu.section", + icon="package", + order=20, + items=[ + MenuItemDefinition( + id="catalog.products", + label_key="catalog.menu.products", + icon="box", + route="/admin/catalog/products", + order=10 + ), + ] + ) + """ + + id: str + label_key: str | None + icon: str | None = None + order: int = 100 + items: list[MenuItemDefinition] = field(default_factory=list) + is_super_admin_only: bool = False + is_collapsible: bool = True + + @dataclass class ScheduledTask: """ @@ -190,6 +277,14 @@ class ModuleDefinition: menu_items: dict[FrontendType, list[str]] = field(default_factory=dict) permissions: list[str] = field(default_factory=list) + # ========================================================================= + # Menu Definitions (Module-Driven Menus) + # ========================================================================= + # NEW: Full menu definitions per frontend type. When set, these take + # precedence over menu_items for menu rendering. This enables modules + # to fully define their own menu structure with icons, routes, and labels. + menus: dict[FrontendType, list[MenuSectionDefinition]] = field(default_factory=dict) + # ========================================================================= # Classification # ========================================================================= @@ -235,15 +330,15 @@ class ModuleDefinition: scheduled_tasks: list[ScheduledTask] = field(default_factory=list) # ========================================================================= - # Menu Item Methods + # Menu Item Methods (Legacy - uses menu_items dict of IDs) # ========================================================================= def get_menu_items(self, frontend_type: FrontendType) -> list[str]: - """Get menu item IDs for a specific frontend type.""" + """Get menu item IDs for a specific frontend type (legacy).""" return self.menu_items.get(frontend_type, []) def get_all_menu_items(self) -> set[str]: - """Get all menu item IDs across all frontend types.""" + """Get all menu item IDs across all frontend types (legacy).""" all_items = set() for items in self.menu_items.values(): all_items.update(items) @@ -253,6 +348,50 @@ class ModuleDefinition: """Check if this module provides a specific menu item.""" return menu_item_id in self.get_all_menu_items() + # ========================================================================= + # Menu Definition Methods (New - uses menus dict of full definitions) + # ========================================================================= + + def get_menu_sections(self, frontend_type: FrontendType) -> list[MenuSectionDefinition]: + """ + Get menu section definitions for a specific frontend type. + + Args: + frontend_type: The frontend type to get menus for + + Returns: + List of MenuSectionDefinition objects, sorted by order + """ + sections = self.menus.get(frontend_type, []) + return sorted(sections, key=lambda s: s.order) + + def get_all_menu_definitions(self) -> dict[FrontendType, list[MenuSectionDefinition]]: + """ + Get all menu definitions for all frontend types. + + Returns: + Dict mapping FrontendType to list of MenuSectionDefinition + """ + return self.menus + + def has_menus_for_frontend(self, frontend_type: FrontendType) -> bool: + """Check if this module has menu definitions for a frontend type.""" + return frontend_type in self.menus and len(self.menus[frontend_type]) > 0 + + def get_mandatory_menu_item_ids(self, frontend_type: FrontendType) -> set[str]: + """ + Get IDs of all mandatory menu items for a frontend type. + + Returns: + Set of menu item IDs that are marked as is_mandatory=True + """ + mandatory_ids = set() + for section in self.menus.get(frontend_type, []): + for item in section.items: + if item.is_mandatory: + mandatory_ids.add(item.id) + return mandatory_ids + # ========================================================================= # Feature Methods # ========================================================================= diff --git a/app/modules/billing/definition.py b/app/modules/billing/definition.py index b162edbd..bd4c09de 100644 --- a/app/modules/billing/definition.py +++ b/app/modules/billing/definition.py @@ -6,8 +6,8 @@ Defines the billing module including its features, menu items, route configurations, and scheduled tasks. """ -from app.modules.base import ModuleDefinition, ScheduledTask -from models.database.admin_menu_config import FrontendType +from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, ScheduledTask +from app.modules.enums import FrontendType def _get_admin_router(): @@ -53,6 +53,72 @@ billing_module = ModuleDefinition( "invoices", # Vendor invoice history ], }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="billing", + label_key="billing.menu.billing_subscriptions", + icon="credit-card", + order=50, + items=[ + MenuItemDefinition( + id="subscription-tiers", + label_key="billing.menu.subscription_tiers", + icon="tag", + route="/admin/subscription-tiers", + order=10, + ), + MenuItemDefinition( + id="subscriptions", + label_key="billing.menu.vendor_subscriptions", + icon="credit-card", + route="/admin/subscriptions", + order=20, + ), + MenuItemDefinition( + id="billing-history", + label_key="billing.menu.billing_history", + icon="document-text", + route="/admin/billing-history", + order=30, + ), + ], + ), + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="sales", + label_key="billing.menu.sales_orders", + icon="currency-euro", + order=20, + items=[ + MenuItemDefinition( + id="invoices", + label_key="billing.menu.invoices", + icon="currency-euro", + route="/vendor/{vendor_code}/invoices", + order=30, + ), + ], + ), + MenuSectionDefinition( + id="account", + label_key="billing.menu.account_settings", + icon="credit-card", + order=900, + items=[ + MenuItemDefinition( + id="billing", + label_key="billing.menu.billing", + icon="credit-card", + route="/vendor/{vendor_code}/billing", + order=30, + ), + ], + ), + ], + }, is_core=False, # Billing can be disabled (e.g., internal platforms) # ========================================================================= # Self-Contained Module Configuration diff --git a/app/modules/billing/locales/de.json b/app/modules/billing/locales/de.json index 77992c4d..0a2c39af 100644 --- a/app/modules/billing/locales/de.json +++ b/app/modules/billing/locales/de.json @@ -89,7 +89,17 @@ "payment_method_updated": "Zahlungsmethode aktualisiert", "subscription_cancelled": "Abonnement gekündigt", "error_loading": "Fehler beim Laden der Abrechnungsinformationen", - "error_updating": "Fehler beim Aktualisieren des Abonnements" + "error_updating": "Fehler beim Aktualisieren des Abonnements", + "failed_to_load_billing_data": "Failed to load billing data", + "failed_to_create_checkout_session": "Failed to create checkout session", + "failed_to_open_payment_portal": "Failed to open payment portal", + "subscription_cancelled_you_have_access_u": "Subscription cancelled. You have access until the end of your billing period.", + "failed_to_cancel_subscription": "Failed to cancel subscription", + "subscription_reactivated": "Subscription reactivated!", + "failed_to_reactivate_subscription": "Failed to reactivate subscription", + "failed_to_purchase_addon": "Failed to purchase add-on", + "addon_cancelled_successfully": "Add-on cancelled successfully", + "failed_to_cancel_addon": "Failed to cancel add-on" }, "limits": { "orders_exceeded": "Monatliches Bestelllimit erreicht. Upgrade für mehr.", diff --git a/app/modules/billing/locales/en.json b/app/modules/billing/locales/en.json index b37e8513..ffab8e16 100644 --- a/app/modules/billing/locales/en.json +++ b/app/modules/billing/locales/en.json @@ -89,7 +89,17 @@ "payment_method_updated": "Payment method updated", "subscription_cancelled": "Subscription cancelled", "error_loading": "Error loading billing information", - "error_updating": "Error updating subscription" + "error_updating": "Error updating subscription", + "failed_to_load_billing_data": "Failed to load billing data", + "failed_to_create_checkout_session": "Failed to create checkout session", + "failed_to_open_payment_portal": "Failed to open payment portal", + "subscription_cancelled_you_have_access_u": "Subscription cancelled. You have access until the end of your billing period.", + "failed_to_cancel_subscription": "Failed to cancel subscription", + "subscription_reactivated": "Subscription reactivated!", + "failed_to_reactivate_subscription": "Failed to reactivate subscription", + "failed_to_purchase_addon": "Failed to purchase add-on", + "addon_cancelled_successfully": "Add-on cancelled successfully", + "failed_to_cancel_addon": "Failed to cancel add-on" }, "limits": { "orders_exceeded": "Monthly order limit reached. Upgrade to continue.", diff --git a/app/modules/billing/locales/fr.json b/app/modules/billing/locales/fr.json index 36d79b7a..1829825a 100644 --- a/app/modules/billing/locales/fr.json +++ b/app/modules/billing/locales/fr.json @@ -89,7 +89,17 @@ "payment_method_updated": "Moyen de paiement mis à jour", "subscription_cancelled": "Abonnement annulé", "error_loading": "Erreur lors du chargement des informations de facturation", - "error_updating": "Erreur lors de la mise à jour de l'abonnement" + "error_updating": "Erreur lors de la mise à jour de l'abonnement", + "failed_to_load_billing_data": "Failed to load billing data", + "failed_to_create_checkout_session": "Failed to create checkout session", + "failed_to_open_payment_portal": "Failed to open payment portal", + "subscription_cancelled_you_have_access_u": "Subscription cancelled. You have access until the end of your billing period.", + "failed_to_cancel_subscription": "Failed to cancel subscription", + "subscription_reactivated": "Subscription reactivated!", + "failed_to_reactivate_subscription": "Failed to reactivate subscription", + "failed_to_purchase_addon": "Failed to purchase add-on", + "addon_cancelled_successfully": "Add-on cancelled successfully", + "failed_to_cancel_addon": "Failed to cancel add-on" }, "limits": { "orders_exceeded": "Limite mensuelle de commandes atteinte. Passez à un niveau supérieur.", diff --git a/app/modules/billing/locales/lb.json b/app/modules/billing/locales/lb.json index adac55ef..2622d438 100644 --- a/app/modules/billing/locales/lb.json +++ b/app/modules/billing/locales/lb.json @@ -89,7 +89,17 @@ "payment_method_updated": "Zuelungsmethod aktualiséiert", "subscription_cancelled": "Abonnement gekënnegt", "error_loading": "Feeler beim Lueden vun de Rechnungsinformatiounen", - "error_updating": "Feeler beim Aktualiséieren vum Abonnement" + "error_updating": "Feeler beim Aktualiséieren vum Abonnement", + "failed_to_load_billing_data": "Failed to load billing data", + "failed_to_create_checkout_session": "Failed to create checkout session", + "failed_to_open_payment_portal": "Failed to open payment portal", + "subscription_cancelled_you_have_access_u": "Subscription cancelled. You have access until the end of your billing period.", + "failed_to_cancel_subscription": "Failed to cancel subscription", + "subscription_reactivated": "Subscription reactivated!", + "failed_to_reactivate_subscription": "Failed to reactivate subscription", + "failed_to_purchase_addon": "Failed to purchase add-on", + "addon_cancelled_successfully": "Add-on cancelled successfully", + "failed_to_cancel_addon": "Failed to cancel add-on" }, "limits": { "orders_exceeded": "Monatlech Bestellungslimit erreecht. Upgrade fir méi.", diff --git a/app/modules/billing/routes/api/admin.py b/app/modules/billing/routes/api/admin.py index a842f7ee..dcc6fddd 100644 --- a/app/modules/billing/routes/api/admin.py +++ b/app/modules/billing/routes/api/admin.py @@ -18,7 +18,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, require_module_access from app.core.database import get_db from app.modules.billing.services import admin_subscription_service, subscription_service -from models.database.user import User +from app.modules.tenancy.models import User from app.modules.billing.schemas import ( BillingHistoryListResponse, BillingHistoryWithVendor, diff --git a/app/modules/billing/routes/api/vendor.py b/app/modules/billing/routes/api/vendor.py index 9af3a65b..6a0ba682 100644 --- a/app/modules/billing/routes/api/vendor.py +++ b/app/modules/billing/routes/api/vendor.py @@ -20,7 +20,7 @@ from app.api.deps import get_current_vendor_api, require_module_access from app.core.config import settings from app.core.database import get_db from app.modules.billing.services import billing_service, subscription_service -from models.database.user import User +from app.modules.tenancy.models import User logger = logging.getLogger(__name__) diff --git a/app/modules/billing/routes/pages/admin.py b/app/modules/billing/routes/pages/admin.py index 1897b949..157df2b3 100644 --- a/app/modules/billing/routes/pages/admin.py +++ b/app/modules/billing/routes/pages/admin.py @@ -15,8 +15,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context from app.templates_config import templates -from models.database.admin_menu_config import FrontendType -from models.database.user import User +from app.modules.enums import FrontendType +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/billing/routes/pages/vendor.py b/app/modules/billing/routes/pages/vendor.py index 92f86e64..0e475bf5 100644 --- a/app/modules/billing/routes/pages/vendor.py +++ b/app/modules/billing/routes/pages/vendor.py @@ -14,7 +14,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_vendor_context from app.templates_config import templates -from models.database.user import User +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/billing/services/admin_subscription_service.py b/app/modules/billing/services/admin_subscription_service.py index 402eec66..6da20d90 100644 --- a/app/modules/billing/services/admin_subscription_service.py +++ b/app/modules/billing/services/admin_subscription_service.py @@ -28,7 +28,7 @@ from app.modules.billing.models import ( VendorSubscription, ) from app.modules.catalog.models import Product -from models.database.vendor import Vendor, VendorUser +from app.modules.tenancy.models import Vendor, VendorUser logger = logging.getLogger(__name__) diff --git a/app/modules/billing/services/billing_service.py b/app/modules/billing/services/billing_service.py index 6bd0b798..febd50f8 100644 --- a/app/modules/billing/services/billing_service.py +++ b/app/modules/billing/services/billing_service.py @@ -23,7 +23,7 @@ from app.modules.billing.models import ( VendorAddOn, VendorSubscription, ) -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/billing/services/capacity_forecast_service.py b/app/modules/billing/services/capacity_forecast_service.py index ff18621e..dea91db6 100644 --- a/app/modules/billing/services/capacity_forecast_service.py +++ b/app/modules/billing/services/capacity_forecast_service.py @@ -22,7 +22,7 @@ from app.modules.billing.models import ( SubscriptionStatus, VendorSubscription, ) -from models.database.vendor import Vendor, VendorUser +from app.modules.tenancy.models import Vendor, VendorUser logger = logging.getLogger(__name__) diff --git a/app/modules/billing/services/stripe_service.py b/app/modules/billing/services/stripe_service.py index 343a2eca..6e7ca652 100644 --- a/app/modules/billing/services/stripe_service.py +++ b/app/modules/billing/services/stripe_service.py @@ -27,7 +27,7 @@ from app.modules.billing.models import ( SubscriptionTier, VendorSubscription, ) -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) @@ -308,7 +308,7 @@ class StripeService: customer_id = subscription.stripe_customer_id else: # Get vendor owner email - from models.database.vendor import VendorUser + from app.modules.tenancy.models import VendorUser owner = ( db.query(VendorUser) diff --git a/app/modules/billing/services/subscription_service.py b/app/modules/billing/services/subscription_service.py index 7254dd8b..7b97cbae 100644 --- a/app/modules/billing/services/subscription_service.py +++ b/app/modules/billing/services/subscription_service.py @@ -46,7 +46,7 @@ from app.modules.billing.schemas import ( UsageSummary, ) from app.modules.catalog.models import Product -from models.database.vendor import Vendor, VendorUser +from app.modules.tenancy.models import Vendor, VendorUser logger = logging.getLogger(__name__) diff --git a/app/modules/billing/static/vendor/js/billing.js b/app/modules/billing/static/vendor/js/billing.js index 7e5da3ab..c3b53698 100644 --- a/app/modules/billing/static/vendor/js/billing.js +++ b/app/modules/billing/static/vendor/js/billing.js @@ -29,6 +29,9 @@ function vendorBilling() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('billing'); + // Guard against multiple initialization if (window._vendorBillingInitialized) return; window._vendorBillingInitialized = true; @@ -81,7 +84,7 @@ function vendorBilling() { } catch (error) { billingLog.error('Error loading billing data:', error); - Utils.showToast('Failed to load billing data', 'error'); + Utils.showToast(I18n.t('billing.messages.failed_to_load_billing_data'), 'error'); } finally { this.loading = false; } @@ -101,7 +104,7 @@ function vendorBilling() { } } catch (error) { billingLog.error('Error creating checkout:', error); - Utils.showToast('Failed to create checkout session', 'error'); + Utils.showToast(I18n.t('billing.messages.failed_to_create_checkout_session'), 'error'); } }, @@ -113,7 +116,7 @@ function vendorBilling() { } } catch (error) { billingLog.error('Error opening portal:', error); - Utils.showToast('Failed to open payment portal', 'error'); + Utils.showToast(I18n.t('billing.messages.failed_to_open_payment_portal'), 'error'); } }, @@ -125,24 +128,24 @@ function vendorBilling() { }); this.showCancelModal = false; - Utils.showToast('Subscription cancelled. You have access until the end of your billing period.', 'success'); + Utils.showToast(I18n.t('billing.messages.subscription_cancelled_you_have_access_u'), 'success'); await this.loadData(); } catch (error) { billingLog.error('Error cancelling subscription:', error); - Utils.showToast('Failed to cancel subscription', 'error'); + Utils.showToast(I18n.t('billing.messages.failed_to_cancel_subscription'), 'error'); } }, async reactivate() { try { await apiClient.post('/vendor/billing/reactivate', {}); - Utils.showToast('Subscription reactivated!', 'success'); + Utils.showToast(I18n.t('billing.messages.subscription_reactivated'), 'success'); await this.loadData(); } catch (error) { billingLog.error('Error reactivating subscription:', error); - Utils.showToast('Failed to reactivate subscription', 'error'); + Utils.showToast(I18n.t('billing.messages.failed_to_reactivate_subscription'), 'error'); } }, @@ -159,7 +162,7 @@ function vendorBilling() { } } catch (error) { billingLog.error('Error purchasing addon:', error); - Utils.showToast('Failed to purchase add-on', 'error'); + Utils.showToast(I18n.t('billing.messages.failed_to_purchase_addon'), 'error'); } finally { this.purchasingAddon = null; } @@ -172,11 +175,11 @@ function vendorBilling() { try { await apiClient.delete(`/vendor/billing/addons/${addon.id}`); - Utils.showToast('Add-on cancelled successfully', 'success'); + Utils.showToast(I18n.t('billing.messages.addon_cancelled_successfully'), 'success'); await this.loadData(); } catch (error) { billingLog.error('Error cancelling addon:', error); - Utils.showToast('Failed to cancel add-on', 'error'); + Utils.showToast(I18n.t('billing.messages.failed_to_cancel_addon'), 'error'); } }, diff --git a/app/modules/billing/templates/billing/public/pricing.html b/app/modules/billing/templates/billing/public/pricing.html index e12cd1e2..0d94c005 100644 --- a/app/modules/billing/templates/billing/public/pricing.html +++ b/app/modules/billing/templates/billing/public/pricing.html @@ -2,7 +2,7 @@ {# Standalone Pricing Page #} {% extends "public/base.html" %} -{% block title %}{{ _("platform.pricing.title") }} - Wizamart{% endblock %} +{% block title %}{{ _("cms.platform.pricing.title") }} - Wizamart{% endblock %} {% block content %}
- {{ _("platform.pricing.trial_note", trial_days=trial_days) }} + {{ _("cms.platform.pricing.trial_note", trial_days=trial_days) }}
{# Billing Toggle #}- {{ _("platform.success.subtitle", trial_days=trial_days) }} + {{ _("cms.platform.success.subtitle", trial_days=trial_days) }}
{# Next Steps #}- {{ _("platform.success.need_help") }} - {{ _("platform.success.contact_support") }} + {{ _("cms.platform.success.need_help") }} + {{ _("cms.platform.success.contact_support") }}
diff --git a/app/modules/cart/routes/api/storefront.py b/app/modules/cart/routes/api/storefront.py index a2f04ead..fc9642d0 100644 --- a/app/modules/cart/routes/api/storefront.py +++ b/app/modules/cart/routes/api/storefront.py @@ -24,7 +24,7 @@ from app.modules.cart.schemas import ( UpdateCartItemRequest, ) from middleware.vendor_context import require_vendor_context -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/modules/catalog/definition.py b/app/modules/catalog/definition.py index d6e7c8f3..da22e568 100644 --- a/app/modules/catalog/definition.py +++ b/app/modules/catalog/definition.py @@ -1,7 +1,8 @@ # app/modules/catalog/definition.py """Catalog module definition.""" -from app.modules.base import ModuleDefinition +from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition +from app.modules.enums import FrontendType module = ModuleDefinition( code="catalog", @@ -10,4 +11,25 @@ module = ModuleDefinition( version="1.0.0", is_self_contained=True, requires=["inventory"], + # New module-driven menu definitions + menus={ + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="products", + label_key="catalog.menu.products_inventory", + icon="package", + order=10, + items=[ + MenuItemDefinition( + id="products", + label_key="catalog.menu.all_products", + icon="shopping-bag", + route="/vendor/{vendor_code}/products", + order=10, + is_mandatory=True, + ), + ], + ), + ], + }, ) diff --git a/app/modules/catalog/locales/de.json b/app/modules/catalog/locales/de.json index b1271d1a..fc421bd3 100644 --- a/app/modules/catalog/locales/de.json +++ b/app/modules/catalog/locales/de.json @@ -1,49 +1,64 @@ { - "title": "Produktkatalog", - "description": "Produktkatalogverwaltung für Händler", "products": { "title": "Produkte", - "subtitle": "Verwalten Sie Ihren Produktkatalog", - "create": "Produkt erstellen", - "edit": "Produkt bearbeiten", - "delete": "Produkt löschen", - "empty": "Keine Produkte gefunden", - "empty_search": "Keine Produkte entsprechen Ihrer Suche" - }, - "product": { - "name": "Produktname", - "description": "Beschreibung", - "sku": "Artikelnummer", + "product": "Produkt", + "add_product": "Produkt hinzufügen", + "edit_product": "Produkt bearbeiten", + "delete_product": "Produkt löschen", + "product_name": "Produktname", + "product_code": "Produktcode", + "sku": "SKU", "price": "Preis", - "stock": "Bestand", - "status": "Status", - "active": "Aktiv", - "inactive": "Inaktiv" - }, - "media": { - "title": "Produktmedien", - "upload": "Bild hochladen", - "delete": "Bild löschen", - "primary": "Als Hauptbild festlegen", - "error": "Medien-Upload fehlgeschlagen" - }, - "validation": { - "name_required": "Produktname ist erforderlich", - "price_required": "Preis ist erforderlich", - "invalid_sku": "Ungültiges Artikelnummernformat", - "duplicate_sku": "Artikelnummer existiert bereits" + "sale_price": "Verkaufspreis", + "cost": "Kosten", + "stock": "Lagerbestand", + "in_stock": "Auf Lager", + "out_of_stock": "Nicht auf Lager", + "low_stock": "Geringer Bestand", + "availability": "Verfügbarkeit", + "available": "Verfügbar", + "unavailable": "Nicht verfügbar", + "brand": "Marke", + "category": "Kategorie", + "categories": "Kategorien", + "image": "Bild", + "images": "Bilder", + "main_image": "Hauptbild", + "gallery": "Galerie", + "weight": "Gewicht", + "dimensions": "Abmessungen", + "color": "Farbe", + "size": "Größe", + "material": "Material", + "condition": "Zustand", + "new": "Neu", + "used": "Gebraucht", + "refurbished": "Generalüberholt", + "no_products": "Keine Produkte gefunden", + "search_products": "Produkte suchen...", + "filter_by_category": "Nach Kategorie filtern", + "filter_by_status": "Nach Status filtern", + "sort_by": "Sortieren nach", + "sort_newest": "Neueste", + "sort_oldest": "Älteste", + "sort_price_low": "Preis: Niedrig bis Hoch", + "sort_price_high": "Preis: Hoch bis Niedrig", + "sort_name_az": "Name: A-Z", + "sort_name_za": "Name: Z-A" }, "messages": { - "created": "Produkt erfolgreich erstellt", - "updated": "Produkt erfolgreich aktualisiert", - "deleted": "Produkt erfolgreich gelöscht", - "not_found": "Produkt nicht gefunden", - "cannot_delete": "Produkt kann nicht gelöscht werden", - "error_loading": "Fehler beim Laden der Produkte" - }, - "filters": { - "all_products": "Alle Produkte", - "active_only": "Nur aktive", - "search_placeholder": "Produkte suchen..." + "product_deleted_successfully": "Product deleted successfully", + "please_fill_in_all_required_fields": "Please fill in all required fields", + "product_updated_successfully": "Product updated successfully", + "failed_to_load_media_library": "Failed to load media library", + "no_vendor_associated_with_this_product": "No vendor associated with this product", + "please_select_an_image_file": "Please select an image file", + "image_must_be_less_than_10mb": "Image must be less than 10MB", + "image_uploaded_successfully": "Image uploaded successfully", + "product_removed_from_vendor_catalog": "Product removed from vendor catalog.", + "please_select_a_vendor": "Please select a vendor", + "please_enter_a_product_title_english": "Please enter a product title (English)", + "product_created_successfully": "Product created successfully", + "please_select_a_vendor_first": "Please select a vendor first" } } diff --git a/app/modules/catalog/locales/fr.json b/app/modules/catalog/locales/fr.json index 08558278..05707fe8 100644 --- a/app/modules/catalog/locales/fr.json +++ b/app/modules/catalog/locales/fr.json @@ -1,49 +1,64 @@ { - "title": "Catalogue produits", - "description": "Gestion du catalogue produits pour les vendeurs", "products": { "title": "Produits", - "subtitle": "Gérez votre catalogue de produits", - "create": "Créer un produit", - "edit": "Modifier le produit", - "delete": "Supprimer le produit", - "empty": "Aucun produit trouvé", - "empty_search": "Aucun produit ne correspond à votre recherche" - }, - "product": { - "name": "Nom du produit", - "description": "Description", - "sku": "Référence", + "product": "Produit", + "add_product": "Ajouter un produit", + "edit_product": "Modifier le produit", + "delete_product": "Supprimer le produit", + "product_name": "Nom du produit", + "product_code": "Code produit", + "sku": "SKU", "price": "Prix", + "sale_price": "Prix de vente", + "cost": "Coût", "stock": "Stock", - "status": "Statut", - "active": "Actif", - "inactive": "Inactif" - }, - "media": { - "title": "Médias du produit", - "upload": "Télécharger une image", - "delete": "Supprimer l'image", - "primary": "Définir comme image principale", - "error": "Échec du téléchargement" - }, - "validation": { - "name_required": "Le nom du produit est requis", - "price_required": "Le prix est requis", - "invalid_sku": "Format de référence invalide", - "duplicate_sku": "La référence existe déjà" + "in_stock": "En stock", + "out_of_stock": "Rupture de stock", + "low_stock": "Stock faible", + "availability": "Disponibilité", + "available": "Disponible", + "unavailable": "Indisponible", + "brand": "Marque", + "category": "Catégorie", + "categories": "Catégories", + "image": "Image", + "images": "Images", + "main_image": "Image principale", + "gallery": "Galerie", + "weight": "Poids", + "dimensions": "Dimensions", + "color": "Couleur", + "size": "Taille", + "material": "Matériau", + "condition": "État", + "new": "Neuf", + "used": "Occasion", + "refurbished": "Reconditionné", + "no_products": "Aucun produit trouvé", + "search_products": "Rechercher des produits...", + "filter_by_category": "Filtrer par catégorie", + "filter_by_status": "Filtrer par statut", + "sort_by": "Trier par", + "sort_newest": "Plus récent", + "sort_oldest": "Plus ancien", + "sort_price_low": "Prix : croissant", + "sort_price_high": "Prix : décroissant", + "sort_name_az": "Nom : A-Z", + "sort_name_za": "Nom : Z-A" }, "messages": { - "created": "Produit créé avec succès", - "updated": "Produit mis à jour avec succès", - "deleted": "Produit supprimé avec succès", - "not_found": "Produit non trouvé", - "cannot_delete": "Impossible de supprimer le produit", - "error_loading": "Erreur lors du chargement des produits" - }, - "filters": { - "all_products": "Tous les produits", - "active_only": "Actifs uniquement", - "search_placeholder": "Rechercher des produits..." + "product_deleted_successfully": "Product deleted successfully", + "please_fill_in_all_required_fields": "Please fill in all required fields", + "product_updated_successfully": "Product updated successfully", + "failed_to_load_media_library": "Failed to load media library", + "no_vendor_associated_with_this_product": "No vendor associated with this product", + "please_select_an_image_file": "Please select an image file", + "image_must_be_less_than_10mb": "Image must be less than 10MB", + "image_uploaded_successfully": "Image uploaded successfully", + "product_removed_from_vendor_catalog": "Product removed from vendor catalog.", + "please_select_a_vendor": "Please select a vendor", + "please_enter_a_product_title_english": "Please enter a product title (English)", + "product_created_successfully": "Product created successfully", + "please_select_a_vendor_first": "Please select a vendor first" } } diff --git a/app/modules/catalog/locales/lb.json b/app/modules/catalog/locales/lb.json index 442be57b..9c442288 100644 --- a/app/modules/catalog/locales/lb.json +++ b/app/modules/catalog/locales/lb.json @@ -1,49 +1,64 @@ { - "title": "Produktkatalog", - "description": "Produktkatalogverwaltung fir Händler", "products": { "title": "Produkter", - "subtitle": "Verwalte Äre Produktkatalog", - "create": "Produkt erstellen", - "edit": "Produkt beaarbechten", - "delete": "Produkt läschen", - "empty": "Keng Produkter fonnt", - "empty_search": "Keng Produkter entspriechen Ärer Sich" - }, - "product": { - "name": "Produktnumm", - "description": "Beschreiwung", - "sku": "Artikelnummer", + "product": "Produkt", + "add_product": "Produkt derbäisetzen", + "edit_product": "Produkt änneren", + "delete_product": "Produkt läschen", + "product_name": "Produktnumm", + "product_code": "Produktcode", + "sku": "SKU", "price": "Präis", - "stock": "Bestand", - "status": "Status", - "active": "Aktiv", - "inactive": "Inaktiv" - }, - "media": { - "title": "Produktmedien", - "upload": "Bild eroplueden", - "delete": "Bild läschen", - "primary": "Als Haaptbild setzen", - "error": "Medien-Upload feelgeschloen" - }, - "validation": { - "name_required": "Produktnumm ass erfuerderlech", - "price_required": "Präis ass erfuerderlech", - "invalid_sku": "Ongëlteg Artikelnummerformat", - "duplicate_sku": "Artikelnummer existéiert schonn" + "sale_price": "Verkafspräis", + "cost": "Käschten", + "stock": "Lager", + "in_stock": "Op Lager", + "out_of_stock": "Net op Lager", + "low_stock": "Niddregen Stock", + "availability": "Disponibilitéit", + "available": "Disponibel", + "unavailable": "Net disponibel", + "brand": "Mark", + "category": "Kategorie", + "categories": "Kategorien", + "image": "Bild", + "images": "Biller", + "main_image": "Haaptbild", + "gallery": "Galerie", + "weight": "Gewiicht", + "dimensions": "Dimensiounen", + "color": "Faarf", + "size": "Gréisst", + "material": "Material", + "condition": "Zoustand", + "new": "Nei", + "used": "Gebraucht", + "refurbished": "Iwwerholl", + "no_products": "Keng Produkter fonnt", + "search_products": "Produkter sichen...", + "filter_by_category": "No Kategorie filteren", + "filter_by_status": "No Status filteren", + "sort_by": "Sortéieren no", + "sort_newest": "Neisten", + "sort_oldest": "Eelsten", + "sort_price_low": "Präis: Niddreg op Héich", + "sort_price_high": "Präis: Héich op Niddreg", + "sort_name_az": "Numm: A-Z", + "sort_name_za": "Numm: Z-A" }, "messages": { - "created": "Produkt erfollegräich erstallt", - "updated": "Produkt erfollegräich aktualiséiert", - "deleted": "Produkt erfollegräich geläscht", - "not_found": "Produkt net fonnt", - "cannot_delete": "Produkt kann net geläscht ginn", - "error_loading": "Feeler beim Lueden vun de Produkter" - }, - "filters": { - "all_products": "All Produkter", - "active_only": "Nëmmen aktiv", - "search_placeholder": "Produkter sichen..." + "product_deleted_successfully": "Product deleted successfully", + "please_fill_in_all_required_fields": "Please fill in all required fields", + "product_updated_successfully": "Product updated successfully", + "failed_to_load_media_library": "Failed to load media library", + "no_vendor_associated_with_this_product": "No vendor associated with this product", + "please_select_an_image_file": "Please select an image file", + "image_must_be_less_than_10mb": "Image must be less than 10MB", + "image_uploaded_successfully": "Image uploaded successfully", + "product_removed_from_vendor_catalog": "Product removed from vendor catalog.", + "please_select_a_vendor": "Please select a vendor", + "please_enter_a_product_title_english": "Please enter a product title (English)", + "product_created_successfully": "Product created successfully", + "please_select_a_vendor_first": "Please select a vendor first" } } diff --git a/app/modules/catalog/routes/api/storefront.py b/app/modules/catalog/routes/api/storefront.py index 22f13586..19cdbf45 100644 --- a/app/modules/catalog/routes/api/storefront.py +++ b/app/modules/catalog/routes/api/storefront.py @@ -22,7 +22,7 @@ from app.modules.catalog.schemas import ( ProductResponse, ) from middleware.vendor_context import require_vendor_context -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/modules/catalog/routes/pages/admin.py b/app/modules/catalog/routes/pages/admin.py index 1d6a0ce5..b24a3247 100644 --- a/app/modules/catalog/routes/pages/admin.py +++ b/app/modules/catalog/routes/pages/admin.py @@ -15,8 +15,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context from app.templates_config import templates -from models.database.admin_menu_config import FrontendType -from models.database.user import User +from app.modules.enums import FrontendType +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/catalog/routes/pages/vendor.py b/app/modules/catalog/routes/pages/vendor.py index a637fe82..48ef6375 100644 --- a/app/modules/catalog/routes/pages/vendor.py +++ b/app/modules/catalog/routes/pages/vendor.py @@ -14,7 +14,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_vendor_context from app.templates_config import templates -from models.database.user import User +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/catalog/services/vendor_product_service.py b/app/modules/catalog/services/vendor_product_service.py index 87cfffd4..5dfa204b 100644 --- a/app/modules/catalog/services/vendor_product_service.py +++ b/app/modules/catalog/services/vendor_product_service.py @@ -16,7 +16,7 @@ from sqlalchemy.orm import Session, joinedload from app.modules.catalog.exceptions import ProductNotFoundException from app.modules.catalog.models import Product -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/catalog/static/admin/js/product-create.js b/app/modules/catalog/static/admin/js/product-create.js index 7860e2c3..431a5d3d 100644 --- a/app/modules/catalog/static/admin/js/product-create.js +++ b/app/modules/catalog/static/admin/js/product-create.js @@ -66,6 +66,9 @@ function adminVendorProductCreate() { }, async init() { + // Load i18n translations + await I18n.loadModule('catalog'); + adminVendorProductCreateLog.info('Vendor Product Create init() called'); // Guard against multiple initialization @@ -166,12 +169,12 @@ function adminVendorProductCreate() { */ async createProduct() { if (!this.form.vendor_id) { - Utils.showToast('Please select a vendor', 'error'); + Utils.showToast(I18n.t('catalog.messages.please_select_a_vendor'), 'error'); return; } if (!this.form.translations.en.title?.trim()) { - Utils.showToast('Please enter a product title (English)', 'error'); + Utils.showToast(I18n.t('catalog.messages.please_enter_a_product_title_english'), 'error'); return; } @@ -224,7 +227,7 @@ function adminVendorProductCreate() { adminVendorProductCreateLog.info('Product created:', response.id); - Utils.showToast('Product created successfully', 'success'); + Utils.showToast(I18n.t('catalog.messages.product_created_successfully'), 'success'); // Redirect to the new product's detail page setTimeout(() => { @@ -232,7 +235,7 @@ function adminVendorProductCreate() { }, 1000); } catch (error) { adminVendorProductCreateLog.error('Failed to create product:', error); - Utils.showToast(error.message || 'Failed to create product', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_create_product'), 'error'); } finally { this.saving = false; } @@ -274,7 +277,7 @@ function adminVendorProductCreate() { this.mediaPickerState.total = response.total || 0; } catch (error) { adminVendorProductCreateLog.error('Failed to load media library:', error); - Utils.showToast('Failed to load media library', 'error'); + Utils.showToast(I18n.t('catalog.messages.failed_to_load_media_library'), 'error'); } finally { this.mediaPickerState.loading = false; } @@ -326,17 +329,17 @@ function adminVendorProductCreate() { const vendorId = this.form?.vendor_id; if (!vendorId) { - Utils.showToast('Please select a vendor first', 'error'); + Utils.showToast(I18n.t('catalog.messages.please_select_a_vendor_first'), 'error'); return; } if (!file.type.startsWith('image/')) { - Utils.showToast('Please select an image file', 'error'); + Utils.showToast(I18n.t('catalog.messages.please_select_an_image_file'), 'error'); return; } if (file.size > 10 * 1024 * 1024) { - Utils.showToast('Image must be less than 10MB', 'error'); + Utils.showToast(I18n.t('catalog.messages.image_must_be_less_than_10mb'), 'error'); return; } @@ -355,11 +358,11 @@ function adminVendorProductCreate() { this.mediaPickerState.media.unshift(response.media); this.mediaPickerState.total++; this.toggleMediaSelection(response.media); - Utils.showToast('Image uploaded successfully', 'success'); + Utils.showToast(I18n.t('catalog.messages.image_uploaded_successfully'), 'success'); } } catch (error) { adminVendorProductCreateLog.error('Failed to upload image:', error); - Utils.showToast(error.message || 'Failed to upload image', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_upload_image'), 'error'); } finally { this.mediaPickerState.uploading = false; event.target.value = ''; diff --git a/app/modules/catalog/static/admin/js/product-edit.js b/app/modules/catalog/static/admin/js/product-edit.js index 0ab7d084..021df2b5 100644 --- a/app/modules/catalog/static/admin/js/product-edit.js +++ b/app/modules/catalog/static/admin/js/product-edit.js @@ -76,6 +76,9 @@ function adminVendorProductEdit() { }, async init() { + // Load i18n translations + await I18n.loadModule('catalog'); + adminVendorProductEditLog.info('Vendor Product Edit init() called, ID:', this.productId); // Guard against multiple initialization @@ -209,7 +212,7 @@ function adminVendorProductEdit() { */ async saveProduct() { if (!this.isFormValid()) { - Utils.showToast('Please fill in all required fields', 'error'); + Utils.showToast(I18n.t('catalog.messages.please_fill_in_all_required_fields'), 'error'); return; } @@ -266,7 +269,7 @@ function adminVendorProductEdit() { adminVendorProductEditLog.info('Product saved:', this.productId); - Utils.showToast('Product updated successfully', 'success'); + Utils.showToast(I18n.t('catalog.messages.product_updated_successfully'), 'success'); // Redirect to detail page setTimeout(() => { @@ -274,7 +277,7 @@ function adminVendorProductEdit() { }, 1000); } catch (error) { adminVendorProductEditLog.error('Failed to save product:', error); - Utils.showToast(error.message || 'Failed to save product', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_save_product'), 'error'); } finally { this.saving = false; } @@ -316,7 +319,7 @@ function adminVendorProductEdit() { this.mediaPickerState.total = response.total || 0; } catch (error) { adminVendorProductEditLog.error('Failed to load media library:', error); - Utils.showToast('Failed to load media library', 'error'); + Utils.showToast(I18n.t('catalog.messages.failed_to_load_media_library'), 'error'); } finally { this.mediaPickerState.loading = false; } @@ -368,17 +371,17 @@ function adminVendorProductEdit() { const vendorId = this.product?.vendor_id; if (!vendorId) { - Utils.showToast('No vendor associated with this product', 'error'); + Utils.showToast(I18n.t('catalog.messages.no_vendor_associated_with_this_product'), 'error'); return; } if (!file.type.startsWith('image/')) { - Utils.showToast('Please select an image file', 'error'); + Utils.showToast(I18n.t('catalog.messages.please_select_an_image_file'), 'error'); return; } if (file.size > 10 * 1024 * 1024) { - Utils.showToast('Image must be less than 10MB', 'error'); + Utils.showToast(I18n.t('catalog.messages.image_must_be_less_than_10mb'), 'error'); return; } @@ -397,11 +400,11 @@ function adminVendorProductEdit() { this.mediaPickerState.media.unshift(response.media); this.mediaPickerState.total++; this.toggleMediaSelection(response.media); - Utils.showToast('Image uploaded successfully', 'success'); + Utils.showToast(I18n.t('catalog.messages.image_uploaded_successfully'), 'success'); } } catch (error) { adminVendorProductEditLog.error('Failed to upload image:', error); - Utils.showToast(error.message || 'Failed to upload image', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_upload_image'), 'error'); } finally { this.mediaPickerState.uploading = false; event.target.value = ''; diff --git a/app/modules/catalog/static/admin/js/products.js b/app/modules/catalog/static/admin/js/products.js index fd6284f0..3858fa4a 100644 --- a/app/modules/catalog/static/admin/js/products.js +++ b/app/modules/catalog/static/admin/js/products.js @@ -116,6 +116,9 @@ function adminVendorProducts() { }, async init() { + // Load i18n translations + await I18n.loadModule('catalog'); + adminVendorProductsLog.info('Vendor Products init() called'); // Guard against multiple initialization @@ -385,7 +388,7 @@ function adminVendorProducts() { this.productToRemove = null; // Show success notification - Utils.showToast('Product removed from vendor catalog.', 'success'); + Utils.showToast(I18n.t('catalog.messages.product_removed_from_vendor_catalog'), 'success'); // Refresh the list await this.refresh(); diff --git a/app/modules/catalog/static/vendor/js/products.js b/app/modules/catalog/static/vendor/js/products.js index 014de48a..189ec524 100644 --- a/app/modules/catalog/static/vendor/js/products.js +++ b/app/modules/catalog/static/vendor/js/products.js @@ -112,6 +112,9 @@ function vendorProducts() { }, async init() { + // Load i18n translations + await I18n.loadModule('catalog'); + vendorProductsLog.info('Products init() called'); // Guard against multiple initialization @@ -230,13 +233,13 @@ function vendorProducts() { await apiClient.put(`/vendor/products/${product.id}/toggle-active`); product.is_active = !product.is_active; Utils.showToast( - product.is_active ? 'Product activated' : 'Product deactivated', + product.is_active ? I18n.t('catalog.messages.product_activated') : I18n.t('catalog.messages.product_deactivated'), 'success' ); vendorProductsLog.info('Toggled product active:', product.id, product.is_active); } catch (error) { vendorProductsLog.error('Failed to toggle active:', error); - Utils.showToast(error.message || 'Failed to update product', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_update_product'), 'error'); } finally { this.saving = false; } @@ -251,13 +254,13 @@ function vendorProducts() { await apiClient.put(`/vendor/products/${product.id}/toggle-featured`); product.is_featured = !product.is_featured; Utils.showToast( - product.is_featured ? 'Product marked as featured' : 'Product unmarked as featured', + product.is_featured ? I18n.t('catalog.messages.product_marked_as_featured') : I18n.t('catalog.messages.product_unmarked_as_featured'), 'success' ); vendorProductsLog.info('Toggled product featured:', product.id, product.is_featured); } catch (error) { vendorProductsLog.error('Failed to toggle featured:', error); - Utils.showToast(error.message || 'Failed to update product', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_update_product'), 'error'); } finally { this.saving = false; } @@ -288,7 +291,7 @@ function vendorProducts() { this.saving = true; try { await apiClient.delete(`/vendor/products/${this.selectedProduct.id}`); - Utils.showToast('Product deleted successfully', 'success'); + Utils.showToast(I18n.t('catalog.messages.product_deleted_successfully'), 'success'); vendorProductsLog.info('Deleted product:', this.selectedProduct.id); this.showDeleteModal = false; @@ -296,7 +299,7 @@ function vendorProducts() { await this.loadProducts(); } catch (error) { vendorProductsLog.error('Failed to delete product:', error); - Utils.showToast(error.message || 'Failed to delete product', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_delete_product'), 'error'); } finally { this.saving = false; } @@ -417,12 +420,12 @@ function vendorProducts() { successCount++; } } - Utils.showToast(`${successCount} product(s) activated`, 'success'); + Utils.showToast(I18n.t('catalog.messages.products_activated', { count: successCount }), 'success'); this.clearSelection(); await this.loadProducts(); } catch (error) { vendorProductsLog.error('Bulk activate failed:', error); - Utils.showToast(error.message || 'Failed to activate products', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_activate_products'), 'error'); } finally { this.saving = false; } @@ -445,12 +448,12 @@ function vendorProducts() { successCount++; } } - Utils.showToast(`${successCount} product(s) deactivated`, 'success'); + Utils.showToast(I18n.t('catalog.messages.products_deactivated', { count: successCount }), 'success'); this.clearSelection(); await this.loadProducts(); } catch (error) { vendorProductsLog.error('Bulk deactivate failed:', error); - Utils.showToast(error.message || 'Failed to deactivate products', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_deactivate_products'), 'error'); } finally { this.saving = false; } @@ -473,12 +476,12 @@ function vendorProducts() { successCount++; } } - Utils.showToast(`${successCount} product(s) marked as featured`, 'success'); + Utils.showToast(I18n.t('catalog.messages.products_marked_as_featured', { count: successCount }), 'success'); this.clearSelection(); await this.loadProducts(); } catch (error) { vendorProductsLog.error('Bulk set featured failed:', error); - Utils.showToast(error.message || 'Failed to update products', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_update_product'), 'error'); } finally { this.saving = false; } @@ -501,12 +504,12 @@ function vendorProducts() { successCount++; } } - Utils.showToast(`${successCount} product(s) unmarked as featured`, 'success'); + Utils.showToast(I18n.t('catalog.messages.products_unmarked_as_featured', { count: successCount }), 'success'); this.clearSelection(); await this.loadProducts(); } catch (error) { vendorProductsLog.error('Bulk remove featured failed:', error); - Utils.showToast(error.message || 'Failed to update products', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_update_product'), 'error'); } finally { this.saving = false; } @@ -533,13 +536,13 @@ function vendorProducts() { await apiClient.delete(`/vendor/products/${productId}`); successCount++; } - Utils.showToast(`${successCount} product(s) deleted`, 'success'); + Utils.showToast(I18n.t('catalog.messages.products_deleted', { count: successCount }), 'success'); this.showBulkDeleteModal = false; this.clearSelection(); await this.loadProducts(); } catch (error) { vendorProductsLog.error('Bulk delete failed:', error); - Utils.showToast(error.message || 'Failed to delete products', 'error'); + Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_delete_product'), 'error'); } finally { this.saving = false; } diff --git a/app/modules/checkout/locales/de.json b/app/modules/checkout/locales/de.json index f61cd1bf..521b8350 100644 --- a/app/modules/checkout/locales/de.json +++ b/app/modules/checkout/locales/de.json @@ -1,42 +1,20 @@ { - "title": "Kasse", - "description": "Bestellabwicklung und Zahlungsabwicklung", - "session": { - "title": "Checkout-Sitzung", - "expired": "Sitzung abgelaufen", - "invalid": "Ungültige Sitzung" - }, - "shipping": { - "title": "Lieferadresse", - "select_address": "Adresse auswählen", - "add_new": "Neue Adresse hinzufügen", - "method": "Versandart", - "select_method": "Versandart auswählen", - "not_available": "Für diese Adresse nicht verfügbar" - }, - "payment": { - "title": "Zahlung", - "method": "Zahlungsmethode", - "required": "Zahlung erforderlich", - "failed": "Zahlung fehlgeschlagen" - }, - "order": { - "summary": "Bestellübersicht", - "subtotal": "Zwischensumme", - "shipping": "Versand", - "tax": "MwSt.", - "total": "Gesamtsumme", - "place_order": "Bestellung aufgeben" - }, - "validation": { - "empty_cart": "Warenkorb ist leer", - "invalid_address": "Ungültige Lieferadresse", - "insufficient_inventory": "Unzureichender Bestand" - }, - "messages": { + "storefront": { + "welcome": "Willkommen in unserem Shop", + "browse_products": "Produkte durchstöbern", + "add_to_cart": "In den Warenkorb", + "buy_now": "Jetzt kaufen", + "view_cart": "Warenkorb ansehen", + "checkout": "Zur Kasse", + "continue_shopping": "Weiter einkaufen", + "start_shopping": "Einkaufen starten", + "empty_cart": "Ihr Warenkorb ist leer", + "cart_total": "Warenkorbsumme", + "proceed_checkout": "Zur Kasse gehen", + "payment": "Zahlung", + "place_order": "Bestellung aufgeben", "order_placed": "Bestellung erfolgreich aufgegeben", - "checkout_failed": "Checkout fehlgeschlagen", - "session_expired": "Ihre Sitzung ist abgelaufen", - "inventory_error": "Einige Artikel sind nicht mehr verfügbar" + "thank_you": "Vielen Dank für Ihre Bestellung", + "order_confirmation": "Bestellbestätigung" } } diff --git a/app/modules/checkout/locales/en.json b/app/modules/checkout/locales/en.json index 09d02228..899d2274 100644 --- a/app/modules/checkout/locales/en.json +++ b/app/modules/checkout/locales/en.json @@ -1,42 +1,20 @@ { - "title": "Checkout", - "description": "Order checkout and payment processing", - "session": { - "title": "Checkout Session", - "expired": "Session expired", - "invalid": "Invalid session" - }, - "shipping": { - "title": "Shipping Address", - "select_address": "Select Address", - "add_new": "Add New Address", - "method": "Shipping Method", - "select_method": "Select Shipping Method", - "not_available": "Not available for this address" - }, - "payment": { - "title": "Payment", - "method": "Payment Method", - "required": "Payment required", - "failed": "Payment failed" - }, - "order": { - "summary": "Order Summary", - "subtotal": "Subtotal", - "shipping": "Shipping", - "tax": "Tax", - "total": "Total", - "place_order": "Place Order" - }, - "validation": { - "empty_cart": "Cart is empty", - "invalid_address": "Invalid shipping address", - "insufficient_inventory": "Insufficient inventory" - }, - "messages": { - "order_placed": "Order placed successfully", - "checkout_failed": "Checkout failed", - "session_expired": "Your session has expired", - "inventory_error": "Some items are no longer available" + "storefront": { + "welcome": "Welcome to our store", + "browse_products": "Browse Products", + "add_to_cart": "Add to Cart", + "buy_now": "Buy Now", + "view_cart": "View Cart", + "checkout": "Checkout", + "continue_shopping": "Continue Shopping", + "start_shopping": "Start Shopping", + "empty_cart": "Your cart is empty", + "cart_total": "Cart Total", + "proceed_checkout": "Proceed to Checkout", + "payment": "Payment", + "place_order": "Place Order", + "order_placed": "Order Placed Successfully", + "thank_you": "Thank you for your order", + "order_confirmation": "Order Confirmation" } } diff --git a/app/modules/checkout/locales/fr.json b/app/modules/checkout/locales/fr.json index 3cbe2201..43baeac5 100644 --- a/app/modules/checkout/locales/fr.json +++ b/app/modules/checkout/locales/fr.json @@ -1,42 +1,20 @@ { - "title": "Caisse", - "description": "Traitement des commandes et des paiements", - "session": { - "title": "Session de paiement", - "expired": "Session expirée", - "invalid": "Session invalide" - }, - "shipping": { - "title": "Adresse de livraison", - "select_address": "Sélectionner une adresse", - "add_new": "Ajouter une nouvelle adresse", - "method": "Mode de livraison", - "select_method": "Sélectionner un mode de livraison", - "not_available": "Non disponible pour cette adresse" - }, - "payment": { - "title": "Paiement", - "method": "Mode de paiement", - "required": "Paiement requis", - "failed": "Paiement échoué" - }, - "order": { - "summary": "Récapitulatif de commande", - "subtotal": "Sous-total", - "shipping": "Livraison", - "tax": "TVA", - "total": "Total", - "place_order": "Passer la commande" - }, - "validation": { - "empty_cart": "Le panier est vide", - "invalid_address": "Adresse de livraison invalide", - "insufficient_inventory": "Stock insuffisant" - }, - "messages": { + "storefront": { + "welcome": "Bienvenue dans notre boutique", + "browse_products": "Parcourir les produits", + "add_to_cart": "Ajouter au panier", + "buy_now": "Acheter maintenant", + "view_cart": "Voir le panier", + "checkout": "Paiement", + "continue_shopping": "Continuer vos achats", + "start_shopping": "Commencer vos achats", + "empty_cart": "Votre panier est vide", + "cart_total": "Total du panier", + "proceed_checkout": "Passer à la caisse", + "payment": "Paiement", + "place_order": "Passer la commande", "order_placed": "Commande passée avec succès", - "checkout_failed": "Échec du paiement", - "session_expired": "Votre session a expiré", - "inventory_error": "Certains articles ne sont plus disponibles" + "thank_you": "Merci pour votre commande", + "order_confirmation": "Confirmation de commande" } } diff --git a/app/modules/checkout/locales/lb.json b/app/modules/checkout/locales/lb.json index a0c90033..e0cec9ff 100644 --- a/app/modules/checkout/locales/lb.json +++ b/app/modules/checkout/locales/lb.json @@ -1,42 +1,20 @@ { - "title": "Keess", - "description": "Bestellungsofwécklung a Bezuelung", - "session": { - "title": "Checkout-Sëtzung", - "expired": "Sëtzung ofgelaf", - "invalid": "Ongëlteg Sëtzung" - }, - "shipping": { - "title": "Liwweradress", - "select_address": "Adress auswielen", - "add_new": "Nei Adress derbäisetzen", - "method": "Liwwermethod", - "select_method": "Liwwermethod auswielen", - "not_available": "Net verfügbar fir dës Adress" - }, - "payment": { - "title": "Bezuelung", - "method": "Bezuelungsmethod", - "required": "Bezuelung erfuerderlech", - "failed": "Bezuelung feelgeschloen" - }, - "order": { - "summary": "Bestelliwwersiicht", - "subtotal": "Zwëschesumm", - "shipping": "Liwwerung", - "tax": "MwSt.", - "total": "Gesamtsumm", - "place_order": "Bestellung opginn" - }, - "validation": { - "empty_cart": "Kuerf ass eidel", - "invalid_address": "Ongëlteg Liwweradress", - "insufficient_inventory": "Net genuch Bestand" - }, - "messages": { + "storefront": { + "welcome": "Wëllkomm an eisem Buttek", + "browse_products": "Produkter duerchsichen", + "add_to_cart": "An de Kuerf", + "buy_now": "Elo kafen", + "view_cart": "Kuerf kucken", + "checkout": "Bezuelen", + "continue_shopping": "Weider akafen", + "start_shopping": "Ufänken mat Akafen", + "empty_cart": "Äre Kuerf ass eidel", + "cart_total": "Kuerf Total", + "proceed_checkout": "Zur Bezuelung goen", + "payment": "Bezuelung", + "place_order": "Bestellung opgi", "order_placed": "Bestellung erfollegräich opginn", - "checkout_failed": "Checkout feelgeschloen", - "session_expired": "Är Sëtzung ass ofgelaf", - "inventory_error": "E puer Artikelen sinn net méi verfügbar" + "thank_you": "Merci fir Är Bestellung", + "order_confirmation": "Bestellungsbestätegung" } } diff --git a/app/modules/checkout/routes/api/storefront.py b/app/modules/checkout/routes/api/storefront.py index 87d020ea..66854e8f 100644 --- a/app/modules/checkout/routes/api/storefront.py +++ b/app/modules/checkout/routes/api/storefront.py @@ -30,7 +30,7 @@ from app.modules.customers.schemas import CustomerContext from app.modules.orders.services import order_service from app.modules.messaging.services.email_service import EmailService # noqa: MOD-004 - Core email service from middleware.vendor_context import require_vendor_context -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor from app.modules.orders.schemas import OrderCreate, OrderResponse router = APIRouter() diff --git a/app/modules/cms/definition.py b/app/modules/cms/definition.py index 9bde089a..343cc735 100644 --- a/app/modules/cms/definition.py +++ b/app/modules/cms/definition.py @@ -12,8 +12,8 @@ This is a self-contained module with: - Templates: app.modules.cms.templates (namespaced as cms/) """ -from app.modules.base import ModuleDefinition -from models.database.admin_menu_config import FrontendType +from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition +from app.modules.enums import FrontendType def _get_admin_router(): @@ -53,6 +53,57 @@ cms_module = ModuleDefinition( "media", # Media library ], }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="contentMgmt", + label_key="cms.menu.content_management", + icon="document-text", + order=70, + items=[ + MenuItemDefinition( + id="content-pages", + label_key="cms.menu.content_pages", + icon="document-text", + route="/admin/content-pages", + order=20, + ), + MenuItemDefinition( + id="vendor-themes", + label_key="cms.menu.vendor_themes", + icon="color-swatch", + route="/admin/vendor-themes", + order=30, + ), + ], + ), + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="shop", + label_key="cms.menu.shop_content", + icon="document-text", + order=40, + items=[ + MenuItemDefinition( + id="content-pages", + label_key="cms.menu.content_pages", + icon="document-text", + route="/vendor/{vendor_code}/content-pages", + order=10, + ), + MenuItemDefinition( + id="media", + label_key="cms.menu.media_library", + icon="photograph", + route="/vendor/{vendor_code}/media", + order=20, + ), + ], + ), + ], + }, is_core=True, # CMS is a core module - content management is fundamental # Self-contained module configuration is_self_contained=True, diff --git a/app/modules/cms/locales/de.json b/app/modules/cms/locales/de.json index 1a5c2299..f56a34e1 100644 --- a/app/modules/cms/locales/de.json +++ b/app/modules/cms/locales/de.json @@ -1,126 +1,203 @@ { - "title": "Content-Verwaltung", - "description": "Verwalten Sie Inhaltsseiten, Medienbibliothek und Händler-Themes", - "pages": { - "title": "Inhaltsseiten", - "subtitle": "Verwalten Sie Plattform- und Händler-Inhaltsseiten", - "create": "Seite erstellen", - "edit": "Seite bearbeiten", - "delete": "Seite löschen", - "list": "Alle Seiten", - "empty": "Keine Seiten gefunden", - "empty_search": "Keine Seiten entsprechen Ihrer Suche", - "create_first": "Erste Seite erstellen" - }, - "page": { - "title": "Seitentitel", - "slug": "Slug", - "slug_help": "URL-sichere Kennung (Kleinbuchstaben, Zahlen, Bindestriche)", - "content": "Inhalt", - "content_format": "Inhaltsformat", - "format_html": "HTML", - "format_markdown": "Markdown", - "platform": "Plattform", - "vendor_override": "Händler-Überschreibung", - "vendor_override_none": "Keine (Plattform-Standard)", - "vendor_override_help_default": "Dies ist eine plattformweite Standardseite", - "vendor_override_help_vendor": "Diese Seite überschreibt den Standard nur für den ausgewählten Händler" - }, - "tiers": { - "platform": "Plattform-Marketing", - "vendor_default": "Händler-Standard", - "vendor_override": "Händler-Überschreibung" - }, - "seo": { - "title": "SEO & Metadaten", - "meta_description": "Meta-Beschreibung", - "meta_description_help": "Zeichen (150-160 empfohlen)", - "meta_keywords": "Meta-Schlüsselwörter", - "meta_keywords_placeholder": "schlüsselwort1, schlüsselwort2, schlüsselwort3" - }, - "navigation": { - "title": "Navigation & Anzeige", - "display_order": "Anzeigereihenfolge", - "display_order_help": "Niedriger = zuerst", - "show_in_header": "Im Header anzeigen", - "show_in_footer": "Im Footer anzeigen", - "show_in_legal": "Im Rechtsbereich anzeigen", - "show_in_legal_help": "Untere Leiste neben dem Copyright" - }, - "publishing": { - "published": "Veröffentlicht", - "draft": "Entwurf", - "publish_help": "Diese Seite öffentlich sichtbar machen" - }, - "homepage": { - "title": "Startseiten-Abschnitte", - "subtitle": "Mehrsprachiger Inhalt", - "loading": "Abschnitte werden geladen...", - "hero": { - "title": "Hero-Abschnitt", - "badge_text": "Badge-Text", - "main_title": "Titel", - "subtitle": "Untertitel", - "buttons": "Schaltflächen", - "add_button": "Schaltfläche hinzufügen" + "platform": { + "nav": { + "pricing": "Preise", + "find_shop": "Finden Sie Ihren Shop", + "start_trial": "Kostenlos testen", + "admin_login": "Admin-Login", + "vendor_login": "Händler-Login", + "toggle_menu": "Menü umschalten", + "toggle_dark_mode": "Dunkelmodus umschalten" }, - "features": { - "title": "Funktionen-Abschnitt", - "section_title": "Abschnittstitel", - "cards": "Funktionskarten", - "add_card": "Karte hinzufügen", - "icon": "Icon-Name", - "feature_title": "Titel", - "feature_description": "Beschreibung" + "hero": { + "badge": "{trial_days}-Tage kostenlose Testversion - Keine Kreditkarte erforderlich", + "title": "Leichtes OMS für Letzshop-Verkäufer", + "subtitle": "Bestellverwaltung, Lager und Rechnungsstellung für den luxemburgischen E-Commerce. Schluss mit Tabellenkalkulationen. Führen Sie Ihr Geschäft.", + "cta_trial": "Kostenlos testen", + "cta_find_shop": "Finden Sie Ihren Letzshop" }, "pricing": { - "title": "Preise-Abschnitt", - "section_title": "Abschnittstitel", - "use_tiers": "Abonnement-Stufen aus der Datenbank verwenden", - "use_tiers_help": "Wenn aktiviert, werden Preiskarten dynamisch aus Ihrer Abonnement-Stufenkonfiguration abgerufen." + "title": "Einfache, transparente Preise", + "subtitle": "Wählen Sie den Plan, der zu Ihrem Unternehmen passt. Alle Pläne beinhalten eine {trial_days}-tägige kostenlose Testversion.", + "monthly": "Monatlich", + "annual": "Jährlich", + "save_months": "Sparen Sie 2 Monate!", + "most_popular": "AM BELIEBTESTEN", + "recommended": "EMPFOHLEN", + "contact_sales": "Kontaktieren Sie uns", + "start_trial": "Kostenlos testen", + "per_month": "/Monat", + "per_year": "/Jahr", + "custom": "Individuell", + "orders_per_month": "{count} Bestellungen/Monat", + "unlimited_orders": "Unbegrenzte Bestellungen", + "products_limit": "{count} Produkte", + "unlimited_products": "Unbegrenzte Produkte", + "team_members": "{count} Teammitglieder", + "unlimited_team": "Unbegrenztes Team", + "letzshop_sync": "Letzshop-Synchronisierung", + "eu_vat_invoicing": "EU-MwSt-Rechnungen", + "analytics_dashboard": "Analyse-Dashboard", + "api_access": "API-Zugang", + "multi_channel": "Multi-Channel-Integration", + "products": "Produkte", + "team_member": "Teammitglied", + "unlimited": "Unbegrenzt", + "order_history": "Monate Bestellhistorie", + "trial_note": "Alle Pläne beinhalten eine {trial_days}-tägige kostenlose Testversion. Keine Kreditkarte erforderlich.", + "back_home": "Zurück zur Startseite" + }, + "features": { + "letzshop_sync": "Letzshop-Synchronisierung", + "inventory_basic": "Grundlegende Lagerverwaltung", + "inventory_locations": "Lagerstandorte", + "inventory_purchase_orders": "Bestellungen", + "invoice_lu": "Luxemburg-MwSt-Rechnungen", + "invoice_eu_vat": "EU-MwSt-Rechnungen", + "invoice_bulk": "Massenrechnungen", + "customer_view": "Kundenliste", + "customer_export": "Kundenexport", + "analytics_dashboard": "Analyse-Dashboard", + "accounting_export": "Buchhaltungsexport", + "api_access": "API-Zugang", + "automation_rules": "Automatisierungsregeln", + "team_roles": "Teamrollen und Berechtigungen", + "white_label": "White-Label-Option", + "multi_vendor": "Multi-Händler-Unterstützung", + "custom_integrations": "Individuelle Integrationen", + "sla_guarantee": "SLA-Garantie", + "dedicated_support": "Dedizierter Kundenbetreuer" + }, + "addons": { + "title": "Erweitern Sie Ihre Plattform", + "subtitle": "Fügen Sie Ihre Marke, professionelle E-Mail und erweiterte Sicherheit hinzu.", + "per_year": "/Jahr", + "per_month": "/Monat", + "custom_domain": "Eigene Domain", + "custom_domain_desc": "Nutzen Sie Ihre eigene Domain (meinedomain.com)", + "premium_ssl": "Premium SSL", + "premium_ssl_desc": "EV-Zertifikat für Vertrauenssiegel", + "email_package": "E-Mail-Paket", + "email_package_desc": "Professionelle E-Mail-Adressen" + }, + "find_shop": { + "title": "Finden Sie Ihren Letzshop", + "subtitle": "Verkaufen Sie bereits auf Letzshop? Geben Sie Ihre Shop-URL ein, um zu beginnen.", + "placeholder": "Geben Sie Ihre Letzshop-URL ein (z.B. letzshop.lu/vendors/mein-shop)", + "button": "Meinen Shop finden", + "claim_shop": "Diesen Shop beanspruchen", + "already_claimed": "Bereits beansprucht", + "no_account": "Sie haben kein Letzshop-Konto?", + "signup_letzshop": "Registrieren Sie sich zuerst bei Letzshop", + "then_connect": ", dann kommen Sie zurück, um Ihren Shop zu verbinden.", + "search_placeholder": "Letzshop-URL oder Shopname eingeben...", + "search_button": "Suchen", + "examples": "Beispiele:", + "claim_button": "Diesen Shop beanspruchen und kostenlos testen", + "not_found": "Wir konnten keinen Letzshop mit dieser URL finden. Bitte überprüfen Sie und versuchen Sie es erneut.", + "or_signup": "Oder registrieren Sie sich ohne Letzshop-Verbindung", + "need_help": "Brauchen Sie Hilfe?", + "no_account_yet": "Sie haben noch kein Letzshop-Konto? Kein Problem!", + "create_letzshop": "Letzshop-Konto erstellen", + "signup_without": "Ohne Letzshop registrieren", + "looking_up": "Suche Ihren Shop...", + "found": "Gefunden:", + "claimed_badge": "Bereits beansprucht" + }, + "signup": { + "step_plan": "Plan wählen", + "step_shop": "Shop beanspruchen", + "step_account": "Konto", + "step_payment": "Zahlung", + "choose_plan": "Wählen Sie Ihren Plan", + "save_percent": "Sparen Sie {percent}%", + "trial_info": "Wir erfassen Ihre Zahlungsdaten, aber Sie werden erst nach Ende der Testphase belastet.", + "connect_shop": "Verbinden Sie Ihren Letzshop", + "connect_optional": "Optional: Verknüpfen Sie Ihr Letzshop-Konto, um Bestellungen automatisch zu synchronisieren.", + "connect_continue": "Verbinden und fortfahren", + "skip_step": "Diesen Schritt überspringen", + "create_account": "Erstellen Sie Ihr Konto", + "first_name": "Vorname", + "last_name": "Nachname", + "company_name": "Firmenname", + "email": "E-Mail", + "password": "Passwort", + "password_hint": "Mindestens 8 Zeichen", + "continue": "Weiter", + "continue_payment": "Weiter zur Zahlung", + "back": "Zurück", + "add_payment": "Zahlungsmethode hinzufügen", + "no_charge_note": "Sie werden erst nach Ablauf Ihrer {trial_days}-tägigen Testphase belastet.", + "processing": "Verarbeitung...", + "start_trial": "Kostenlose Testversion starten", + "creating_account": "Erstelle Ihr Konto..." + }, + "success": { + "title": "Willkommen bei Wizamart!", + "subtitle": "Ihr Konto wurde erstellt und Ihre {trial_days}-tägige kostenlose Testphase hat begonnen.", + "what_next": "Was kommt als Nächstes?", + "step_connect": "Letzshop verbinden:", + "step_connect_desc": "Fügen Sie Ihren API-Schlüssel hinzu, um Bestellungen automatisch zu synchronisieren.", + "step_invoicing": "Rechnungsstellung einrichten:", + "step_invoicing_desc": "Konfigurieren Sie Ihre Rechnungseinstellungen für die luxemburgische Compliance.", + "step_products": "Produkte importieren:", + "step_products_desc": "Synchronisieren Sie Ihren Produktkatalog von Letzshop.", + "go_to_dashboard": "Zum Dashboard", + "login_dashboard": "Zum Dashboard anmelden", + "need_help": "Brauchen Sie Hilfe beim Einstieg?", + "contact_support": "Kontaktieren Sie unser Support-Team" }, "cta": { - "title": "Call-to-Action-Abschnitt", - "main_title": "Titel", - "subtitle": "Untertitel", - "buttons": "Schaltflächen", - "add_button": "Schaltfläche hinzufügen" + "title": "Bereit, Ihre Bestellungen zu optimieren?", + "subtitle": "Schließen Sie sich Letzshop-Händlern an, die Wizamart für ihre Bestellverwaltung vertrauen. Starten Sie heute Ihre {trial_days}-tägige kostenlose Testversion.", + "button": "Kostenlos testen" + }, + "footer": { + "tagline": "Leichtes OMS für Letzshop-Verkäufer. Verwalten Sie Bestellungen, Lager und Rechnungen.", + "quick_links": "Schnelllinks", + "platform": "Plattform", + "contact": "Kontakt", + "copyright": "© {year} Wizamart. Entwickelt für den luxemburgischen E-Commerce.", + "privacy": "Datenschutzerklärung", + "terms": "Nutzungsbedingungen", + "about": "Über uns", + "faq": "FAQ", + "contact_us": "Kontaktieren Sie uns" + }, + "modern": { + "badge_integration": "Offizielle Integration", + "badge_connect": "In 2 Minuten verbinden", + "hero_title_1": "Für den luxemburgischen E-Commerce entwickelt", + "hero_title_2": "Das Back-Office, das Letzshop Ihnen nicht gibt", + "hero_subtitle": "Synchronisieren Sie Bestellungen, verwalten Sie Lager, erstellen Sie Rechnungen mit korrekter MwSt und besitzen Sie Ihre Kundendaten. Alles an einem Ort.", + "cta_trial": "{trial_days}-Tage kostenlos testen", + "cta_how": "Sehen Sie, wie es funktioniert", + "hero_note": "Keine Kreditkarte erforderlich. Einrichtung in 5 Minuten. Jederzeit kündbar.", + "pain_title": "Kommt Ihnen das bekannt vor?", + "pain_subtitle": "Das sind die täglichen Frustrationen von Letzshop-Verkäufern", + "pain_manual": "Manuelle Bestelleingabe", + "pain_manual_desc": "Bestellungen von Letzshop in Tabellenkalkulationen kopieren. Jeden. Einzelnen. Tag.", + "pain_inventory": "Lagerchaos", + "pain_inventory_desc": "Der Bestand in Letzshop stimmt nicht mit der Realität überein. Überverkäufe passieren.", + "pain_vat": "Falsche MwSt-Rechnungen", + "pain_vat_desc": "EU-Kunden brauchen die korrekte MwSt. Ihr Buchhalter beschwert sich.", + "pain_customers": "Verlorene Kunden", + "pain_customers_desc": "Letzshop besitzt Ihre Kundendaten. Sie können nicht retargeten oder Loyalität aufbauen.", + "how_title": "So funktioniert es", + "how_subtitle": "Vom Chaos zur Kontrolle in 4 Schritten", + "how_step1": "Letzshop verbinden", + "how_step1_desc": "Geben Sie Ihre Letzshop-API-Zugangsdaten ein. In 2 Minuten erledigt, keine technischen Kenntnisse erforderlich.", + "how_step2": "Bestellungen kommen rein", + "how_step2_desc": "Bestellungen werden automatisch synchronisiert. Bestätigen und Tracking direkt von Wizamart hinzufügen.", + "how_step3": "Rechnungen erstellen", + "how_step3_desc": "Ein Klick, um konforme PDF-Rechnungen mit korrekter MwSt für jedes EU-Land zu erstellen.", + "how_step4": "Ihr Geschäft ausbauen", + "how_step4_desc": "Exportieren Sie Kunden für Marketing. Verfolgen Sie Lagerbestände. Konzentrieren Sie sich auf den Verkauf, nicht auf Tabellenkalkulationen.", + "features_title": "Alles, was ein Letzshop-Verkäufer braucht", + "features_subtitle": "Die operativen Tools, die Letzshop nicht bietet", + "cta_final_title": "Bereit, die Kontrolle über Ihr Letzshop-Geschäft zu übernehmen?", + "cta_final_subtitle": "Schließen Sie sich luxemburgischen Händlern an, die aufgehört haben, gegen Tabellenkalkulationen zu kämpfen, und begonnen haben, ihr Geschäft auszubauen.", + "cta_final_note": "Keine Kreditkarte erforderlich. Einrichtung in 5 Minuten. Volle Professional-Funktionen während der Testphase." } - }, - "media": { - "title": "Medienbibliothek", - "upload": "Hochladen", - "upload_file": "Datei hochladen", - "delete": "Löschen", - "empty": "Keine Mediendateien", - "upload_first": "Laden Sie Ihre erste Datei hoch" - }, - "themes": { - "title": "Händler-Themes", - "subtitle": "Verwalten Sie Händler-Theme-Anpassungen" - }, - "actions": { - "save": "Speichern", - "saving": "Speichern...", - "update": "Seite aktualisieren", - "create": "Seite erstellen", - "cancel": "Abbrechen", - "back_to_list": "Zurück zur Liste", - "preview": "Vorschau", - "revert_to_default": "Auf Standard zurücksetzen" - }, - "messages": { - "created": "Seite erfolgreich erstellt", - "updated": "Seite erfolgreich aktualisiert", - "deleted": "Seite erfolgreich gelöscht", - "reverted": "Auf Standardseite zurückgesetzt", - "error_loading": "Fehler beim Laden der Seite", - "error_saving": "Fehler beim Speichern der Seite", - "confirm_delete": "Sind Sie sicher, dass Sie diese Seite löschen möchten?" - }, - "filters": { - "all_pages": "Alle Seiten", - "all_platforms": "Alle Plattformen", - "search_placeholder": "Seiten suchen..." } } diff --git a/app/modules/cms/locales/fr.json b/app/modules/cms/locales/fr.json index 776a79a0..8a5fe136 100644 --- a/app/modules/cms/locales/fr.json +++ b/app/modules/cms/locales/fr.json @@ -1,126 +1,203 @@ { - "title": "Gestion de contenu", - "description": "Gestion des pages de contenu, de la bibliothèque de médias et des thèmes", - "pages": { - "title": "Pages de contenu", - "subtitle": "Gérez les pages de contenu de la plateforme et des vendeurs", - "create": "Créer une page", - "edit": "Modifier la page", - "delete": "Supprimer la page", - "list": "Toutes les pages", - "empty": "Aucune page trouvée", - "empty_search": "Aucune page ne correspond à votre recherche", - "create_first": "Créer la première page" - }, - "page": { - "title": "Titre de la page", - "slug": "Slug", - "slug_help": "Identifiant URL (minuscules, chiffres, tirets uniquement)", - "content": "Contenu", - "content_format": "Format du contenu", - "format_html": "HTML", - "format_markdown": "Markdown", - "platform": "Plateforme", - "vendor_override": "Remplacement vendeur", - "vendor_override_none": "Aucun (page par défaut)", - "vendor_override_help_default": "Ceci est une page par défaut pour toute la plateforme", - "vendor_override_help_vendor": "Cette page remplace la page par défaut pour le vendeur sélectionné" - }, - "tiers": { - "platform": "Marketing plateforme", - "vendor_default": "Défaut vendeur", - "vendor_override": "Remplacement vendeur" - }, - "seo": { - "title": "SEO & Métadonnées", - "meta_description": "Meta Description", - "meta_description_help": "caractères (150-160 recommandés)", - "meta_keywords": "Mots-clés", - "meta_keywords_placeholder": "mot-clé1, mot-clé2, mot-clé3" - }, - "navigation": { - "title": "Navigation & Affichage", - "display_order": "Ordre d'affichage", - "display_order_help": "Plus bas = premier", - "show_in_header": "Afficher dans l'en-tête", - "show_in_footer": "Afficher dans le pied de page", - "show_in_legal": "Afficher dans les mentions légales", - "show_in_legal_help": "Barre en bas à côté du copyright" - }, - "publishing": { - "published": "Publié", - "draft": "Brouillon", - "publish_help": "Rendre cette page visible au public" - }, - "homepage": { - "title": "Sections de la page d'accueil", - "subtitle": "Contenu multilingue", - "loading": "Chargement des sections...", - "hero": { - "title": "Section Hero", - "badge_text": "Texte du badge", - "main_title": "Titre", - "subtitle": "Sous-titre", - "buttons": "Boutons", - "add_button": "Ajouter un bouton" + "platform": { + "nav": { + "pricing": "Tarifs", + "find_shop": "Trouvez votre boutique", + "start_trial": "Essai gratuit", + "admin_login": "Connexion Admin", + "vendor_login": "Connexion Vendeur", + "toggle_menu": "Basculer le menu", + "toggle_dark_mode": "Basculer le mode sombre" }, - "features": { - "title": "Section Fonctionnalités", - "section_title": "Titre de la section", - "cards": "Cartes de fonctionnalités", - "add_card": "Ajouter une carte", - "icon": "Nom de l'icône", - "feature_title": "Titre", - "feature_description": "Description" + "hero": { + "badge": "Essai gratuit de {trial_days} jours - Aucune carte de crédit requise", + "title": "OMS léger pour les vendeurs Letzshop", + "subtitle": "Gestion des commandes, stocks et facturation conçue pour le e-commerce luxembourgeois. Arrêtez de jongler avec les tableurs. Gérez votre entreprise.", + "cta_trial": "Essai gratuit", + "cta_find_shop": "Trouvez votre boutique Letzshop" }, "pricing": { - "title": "Section Tarifs", - "section_title": "Titre de la section", - "use_tiers": "Utiliser les niveaux d'abonnement de la base de données", - "use_tiers_help": "Si activé, les cartes de tarifs sont extraites dynamiquement de la configuration des niveaux d'abonnement." + "title": "Tarification simple et transparente", + "subtitle": "Choisissez le plan adapté à votre entreprise. Tous les plans incluent un essai gratuit de {trial_days} jours.", + "monthly": "Mensuel", + "annual": "Annuel", + "save_months": "Économisez 2 mois !", + "most_popular": "LE PLUS POPULAIRE", + "recommended": "RECOMMANDÉ", + "contact_sales": "Contactez-nous", + "start_trial": "Essai gratuit", + "per_month": "/mois", + "per_year": "/an", + "custom": "Sur mesure", + "orders_per_month": "{count} commandes/mois", + "unlimited_orders": "Commandes illimitées", + "products_limit": "{count} produits", + "unlimited_products": "Produits illimités", + "team_members": "{count} membres d'équipe", + "unlimited_team": "Équipe illimitée", + "letzshop_sync": "Synchronisation Letzshop", + "eu_vat_invoicing": "Facturation TVA UE", + "analytics_dashboard": "Tableau de bord analytique", + "api_access": "Accès API", + "multi_channel": "Intégration multi-canal", + "products": "produits", + "team_member": "membre d'équipe", + "unlimited": "Illimité", + "order_history": "mois d'historique", + "trial_note": "Tous les plans incluent un essai gratuit de {trial_days} jours. Aucune carte de crédit requise.", + "back_home": "Retour à l'accueil" + }, + "features": { + "letzshop_sync": "Synchronisation Letzshop", + "inventory_basic": "Gestion de stock de base", + "inventory_locations": "Emplacements d'entrepôt", + "inventory_purchase_orders": "Bons de commande", + "invoice_lu": "Facturation TVA Luxembourg", + "invoice_eu_vat": "Facturation TVA UE", + "invoice_bulk": "Facturation en masse", + "customer_view": "Liste des clients", + "customer_export": "Export clients", + "analytics_dashboard": "Tableau de bord analytique", + "accounting_export": "Export comptable", + "api_access": "Accès API", + "automation_rules": "Règles d'automatisation", + "team_roles": "Rôles et permissions", + "white_label": "Option marque blanche", + "multi_vendor": "Support multi-vendeurs", + "custom_integrations": "Intégrations personnalisées", + "sla_guarantee": "Garantie SLA", + "dedicated_support": "Gestionnaire de compte dédié" + }, + "addons": { + "title": "Améliorez votre plateforme", + "subtitle": "Ajoutez votre marque, e-mail professionnel et sécurité renforcée.", + "per_year": "/an", + "per_month": "/mois", + "custom_domain": "Domaine personnalisé", + "custom_domain_desc": "Utilisez votre propre domaine (mondomaine.com)", + "premium_ssl": "SSL Premium", + "premium_ssl_desc": "Certificat EV pour les badges de confiance", + "email_package": "Pack Email", + "email_package_desc": "Adresses e-mail professionnelles" + }, + "find_shop": { + "title": "Trouvez votre boutique Letzshop", + "subtitle": "Vous vendez déjà sur Letzshop ? Entrez l'URL de votre boutique pour commencer.", + "placeholder": "Entrez votre URL Letzshop (ex: letzshop.lu/vendors/ma-boutique)", + "button": "Trouver ma boutique", + "claim_shop": "Réclamer cette boutique", + "already_claimed": "Déjà réclamée", + "no_account": "Vous n'avez pas de compte Letzshop ?", + "signup_letzshop": "Inscrivez-vous d'abord sur Letzshop", + "then_connect": ", puis revenez connecter votre boutique.", + "search_placeholder": "Entrez l'URL Letzshop ou le nom de la boutique...", + "search_button": "Rechercher", + "examples": "Exemples :", + "claim_button": "Réclamez cette boutique et démarrez l'essai gratuit", + "not_found": "Nous n'avons pas trouvé de boutique Letzshop avec cette URL. Vérifiez et réessayez.", + "or_signup": "Ou inscrivez-vous sans connexion Letzshop", + "need_help": "Besoin d'aide ?", + "no_account_yet": "Vous n'avez pas encore de compte Letzshop ? Pas de problème !", + "create_letzshop": "Créer un compte Letzshop", + "signup_without": "S'inscrire sans Letzshop", + "looking_up": "Recherche de votre boutique...", + "found": "Trouvé :", + "claimed_badge": "Déjà réclamée" + }, + "signup": { + "step_plan": "Choisir le plan", + "step_shop": "Réclamer la boutique", + "step_account": "Compte", + "step_payment": "Paiement", + "choose_plan": "Choisissez votre plan", + "save_percent": "Économisez {percent}%", + "trial_info": "Nous collecterons vos informations de paiement, mais vous ne serez pas débité avant la fin de l'essai.", + "connect_shop": "Connectez votre boutique Letzshop", + "connect_optional": "Optionnel : Liez votre compte Letzshop pour synchroniser automatiquement les commandes.", + "connect_continue": "Connecter et continuer", + "skip_step": "Passer cette étape", + "create_account": "Créez votre compte", + "first_name": "Prénom", + "last_name": "Nom", + "company_name": "Nom de l'entreprise", + "email": "E-mail", + "password": "Mot de passe", + "password_hint": "Minimum 8 caractères", + "continue": "Continuer", + "continue_payment": "Continuer vers le paiement", + "back": "Retour", + "add_payment": "Ajouter un moyen de paiement", + "no_charge_note": "Vous ne serez pas débité avant la fin de votre essai de {trial_days} jours.", + "processing": "Traitement en cours...", + "start_trial": "Démarrer l'essai gratuit", + "creating_account": "Création de votre compte..." + }, + "success": { + "title": "Bienvenue sur Wizamart !", + "subtitle": "Votre compte a été créé et votre essai gratuit de {trial_days} jours a commencé.", + "what_next": "Et maintenant ?", + "step_connect": "Connecter Letzshop :", + "step_connect_desc": "Ajoutez votre clé API pour commencer à synchroniser automatiquement les commandes.", + "step_invoicing": "Configurer la facturation :", + "step_invoicing_desc": "Configurez vos paramètres de facturation pour la conformité luxembourgeoise.", + "step_products": "Importer les produits :", + "step_products_desc": "Synchronisez votre catalogue de produits depuis Letzshop.", + "go_to_dashboard": "Aller au tableau de bord", + "login_dashboard": "Connexion au tableau de bord", + "need_help": "Besoin d'aide pour démarrer ?", + "contact_support": "Contactez notre équipe support" }, "cta": { - "title": "Section Appel à l'action", - "main_title": "Titre", - "subtitle": "Sous-titre", - "buttons": "Boutons", - "add_button": "Ajouter un bouton" + "title": "Prêt à optimiser vos commandes ?", + "subtitle": "Rejoignez les vendeurs Letzshop qui font confiance à Wizamart pour leur gestion de commandes. Commencez votre essai gratuit de {trial_days} jours aujourd'hui.", + "button": "Essai gratuit" + }, + "footer": { + "tagline": "OMS léger pour les vendeurs Letzshop. Gérez commandes, stocks et facturation.", + "quick_links": "Liens rapides", + "platform": "Plateforme", + "contact": "Contact", + "copyright": "© {year} Wizamart. Conçu pour le e-commerce luxembourgeois.", + "privacy": "Politique de confidentialité", + "terms": "Conditions d'utilisation", + "about": "À propos", + "faq": "FAQ", + "contact_us": "Nous contacter" + }, + "modern": { + "badge_integration": "Intégration officielle", + "badge_connect": "Connexion en 2 minutes", + "hero_title_1": "Conçu pour le e-commerce luxembourgeois", + "hero_title_2": "Le back-office que Letzshop ne vous donne pas", + "hero_subtitle": "Synchronisez les commandes, gérez les stocks, générez des factures avec la TVA correcte et possédez vos données clients. Tout en un seul endroit.", + "cta_trial": "Essai gratuit de {trial_days} jours", + "cta_how": "Voir comment ça marche", + "hero_note": "Aucune carte de crédit requise. Configuration en 5 minutes. Annulez à tout moment.", + "pain_title": "Ça vous dit quelque chose ?", + "pain_subtitle": "Ce sont les frustrations quotidiennes des vendeurs Letzshop", + "pain_manual": "Saisie manuelle des commandes", + "pain_manual_desc": "Copier-coller les commandes de Letzshop vers des tableurs. Chaque. Jour.", + "pain_inventory": "Chaos des stocks", + "pain_inventory_desc": "Le stock dans Letzshop ne correspond pas à la réalité. Les surventes arrivent.", + "pain_vat": "Mauvaises factures TVA", + "pain_vat_desc": "Les clients UE ont besoin de la TVA correcte. Votre comptable se plaint.", + "pain_customers": "Clients perdus", + "pain_customers_desc": "Letzshop possède vos données clients. Vous ne pouvez pas les recibler ou fidéliser.", + "how_title": "Comment ça marche", + "how_subtitle": "Du chaos au contrôle en 4 étapes", + "how_step1": "Connecter Letzshop", + "how_step1_desc": "Entrez vos identifiants API Letzshop. Fait en 2 minutes, aucune compétence technique requise.", + "how_step2": "Les commandes arrivent", + "how_step2_desc": "Les commandes se synchronisent automatiquement. Confirmez et ajoutez le suivi directement depuis Wizamart.", + "how_step3": "Générer des factures", + "how_step3_desc": "Un clic pour créer des factures PDF conformes avec la TVA correcte pour tout pays UE.", + "how_step4": "Développez votre entreprise", + "how_step4_desc": "Exportez les clients pour le marketing. Suivez les stocks. Concentrez-vous sur la vente, pas les tableurs.", + "features_title": "Tout ce dont un vendeur Letzshop a besoin", + "features_subtitle": "Les outils opérationnels que Letzshop ne fournit pas", + "cta_final_title": "Prêt à prendre le contrôle de votre entreprise Letzshop ?", + "cta_final_subtitle": "Rejoignez les vendeurs luxembourgeois qui ont arrêté de lutter contre les tableurs et ont commencé à développer leur entreprise.", + "cta_final_note": "Aucune carte de crédit requise. Configuration en 5 minutes. Toutes les fonctionnalités Pro pendant l'essai." } - }, - "media": { - "title": "Bibliothèque de médias", - "upload": "Télécharger", - "upload_file": "Télécharger un fichier", - "delete": "Supprimer", - "empty": "Aucun fichier média", - "upload_first": "Téléchargez votre premier fichier" - }, - "themes": { - "title": "Thèmes vendeurs", - "subtitle": "Gérez les personnalisations de thèmes des vendeurs" - }, - "actions": { - "save": "Enregistrer", - "saving": "Enregistrement...", - "update": "Mettre à jour la page", - "create": "Créer la page", - "cancel": "Annuler", - "back_to_list": "Retour à la liste", - "preview": "Aperçu", - "revert_to_default": "Revenir à la valeur par défaut" - }, - "messages": { - "created": "Page créée avec succès", - "updated": "Page mise à jour avec succès", - "deleted": "Page supprimée avec succès", - "reverted": "Retour à la page par défaut", - "error_loading": "Erreur lors du chargement de la page", - "error_saving": "Erreur lors de l'enregistrement de la page", - "confirm_delete": "Êtes-vous sûr de vouloir supprimer cette page ?" - }, - "filters": { - "all_pages": "Toutes les pages", - "all_platforms": "Toutes les plateformes", - "search_placeholder": "Rechercher des pages..." } } diff --git a/app/modules/cms/locales/lb.json b/app/modules/cms/locales/lb.json index f74b9668..18f8ecb2 100644 --- a/app/modules/cms/locales/lb.json +++ b/app/modules/cms/locales/lb.json @@ -1,126 +1,203 @@ { - "title": "Inhalts-Verwaltung", - "description": "Verwaltet Inhaltsäiten, Mediebibliothéik an Händler-Themen", - "pages": { - "title": "Inhaltsäiten", - "subtitle": "Verwaltet Plattform- an Händler-Inhaltsäiten", - "create": "Säit erstellen", - "edit": "Säit änneren", - "delete": "Säit läschen", - "list": "All Säiten", - "empty": "Keng Säite fonnt", - "empty_search": "Keng Säite passen op Är Sich", - "create_first": "Éischt Säit erstellen" - }, - "page": { - "title": "Säitentitel", - "slug": "Slug", - "slug_help": "URL-sécher Kennung (Klengbuschtawen, Zuelen, Bindestricher)", - "content": "Inhalt", - "content_format": "Inhaltsformat", - "format_html": "HTML", - "format_markdown": "Markdown", - "platform": "Plattform", - "vendor_override": "Händler-Iwwerschreiwung", - "vendor_override_none": "Keng (Plattform-Standard)", - "vendor_override_help_default": "Dëst ass eng plattformwäit Standardsäit", - "vendor_override_help_vendor": "Dës Säit iwwerschreift de Standard nëmme fir de gewielte Händler" - }, - "tiers": { - "platform": "Plattform-Marketing", - "vendor_default": "Händler-Standard", - "vendor_override": "Händler-Iwwerschreiwung" - }, - "seo": { - "title": "SEO & Metadaten", - "meta_description": "Meta-Beschreiwung", - "meta_description_help": "Zeechen (150-160 recommandéiert)", - "meta_keywords": "Meta-Schlësselwierder", - "meta_keywords_placeholder": "schlësselwuert1, schlësselwuert2, schlësselwuert3" - }, - "navigation": { - "title": "Navigatioun & Affichage", - "display_order": "Uweisungsreiefolleg", - "display_order_help": "Méi niddreg = éischt", - "show_in_header": "Am Header weisen", - "show_in_footer": "Am Footer weisen", - "show_in_legal": "Am Rechtsberäich weisen", - "show_in_legal_help": "Ënnescht Leist nieft dem Copyright" - }, - "publishing": { - "published": "Verëffentlecht", - "draft": "Entworf", - "publish_help": "Dës Säit ëffentlech siichtbar maachen" - }, - "homepage": { - "title": "Haaptsäit-Sektiounen", - "subtitle": "Méisproochegen Inhalt", - "loading": "Sektiounen ginn gelueden...", - "hero": { - "title": "Hero-Sektioun", - "badge_text": "Badge-Text", - "main_title": "Titel", - "subtitle": "Ënnertitel", - "buttons": "Knäpp", - "add_button": "Knapp derbäisetzen" + "platform": { + "nav": { + "pricing": "Präisser", + "find_shop": "Fannt Äre Buttek", + "start_trial": "Gratis Testen", + "admin_login": "Admin Login", + "vendor_login": "Händler Login", + "toggle_menu": "Menü wiesselen", + "toggle_dark_mode": "Däischter Modus wiesselen" }, - "features": { - "title": "Funktiounen-Sektioun", - "section_title": "Sektiounstitel", - "cards": "Funktiounskaarten", - "add_card": "Kaart derbäisetzen", - "icon": "Icon-Numm", - "feature_title": "Titel", - "feature_description": "Beschreiwung" + "hero": { + "badge": "{trial_days}-Deeg gratis Testversioun - Keng Kreditkaart néideg", + "title": "Liichtt OMS fir Letzshop Verkeefer", + "subtitle": "Bestellungsverwaltung, Lager an Rechnungsstellung fir de lëtzebuergeschen E-Commerce. Schluss mat Tabellen. Féiert Äert Geschäft.", + "cta_trial": "Gratis Testen", + "cta_find_shop": "Fannt Äre Letzshop Buttek" }, "pricing": { - "title": "Präisser-Sektioun", - "section_title": "Sektiounstitel", - "use_tiers": "Abonnement-Stufen aus der Datebank benotzen", - "use_tiers_help": "Wann aktivéiert, ginn d'Präiskaarten dynamesch aus Ärer Abonnement-Stufekonfiguratioun ofgeruff." + "title": "Einfach, transparent Präisser", + "subtitle": "Wielt de Plang deen zu Ärer Firma passt. All Pläng enthalen eng {trial_days}-Deeg gratis Testversioun.", + "monthly": "Monatslech", + "annual": "Jäerlech", + "save_months": "Spuert 2 Méint!", + "most_popular": "AM BELÉIFSTEN", + "recommended": "EMPFOHLEN", + "contact_sales": "Kontaktéiert eis", + "start_trial": "Gratis Testen", + "per_month": "/Mount", + "per_year": "/Joer", + "custom": "Personnaliséiert", + "orders_per_month": "{count} Bestellungen/Mount", + "unlimited_orders": "Onbegrenzt Bestellungen", + "products_limit": "{count} Produkter", + "unlimited_products": "Onbegrenzt Produkter", + "team_members": "{count} Teammemberen", + "unlimited_team": "Onbegrenzt Team", + "letzshop_sync": "Letzshop Synchronisatioun", + "eu_vat_invoicing": "EU TVA Rechnungen", + "analytics_dashboard": "Analyse Dashboard", + "api_access": "API Zougang", + "multi_channel": "Multi-Channel Integratioun", + "products": "Produkter", + "team_member": "Teammember", + "unlimited": "Onbegrenzt", + "order_history": "Méint Bestellungshistorique", + "trial_note": "All Pläng enthalen eng {trial_days}-Deeg gratis Testversioun. Keng Kreditkaart néideg.", + "back_home": "Zréck op d'Haaptsäit" + }, + "features": { + "letzshop_sync": "Letzshop Synchronisatioun", + "inventory_basic": "Basis Lagerverwaltung", + "inventory_locations": "Lagerstanduerten", + "inventory_purchase_orders": "Bestellungen", + "invoice_lu": "Lëtzebuerg TVA Rechnungen", + "invoice_eu_vat": "EU TVA Rechnungen", + "invoice_bulk": "Massrechnungen", + "customer_view": "Clientelëscht", + "customer_export": "Client Export", + "analytics_dashboard": "Analyse Dashboard", + "accounting_export": "Comptabilitéits Export", + "api_access": "API Zougang", + "automation_rules": "Automatiséierungsreegelen", + "team_roles": "Team Rollen an Autorisatiounen", + "white_label": "White-Label Optioun", + "multi_vendor": "Multi-Händler Ënnerstëtzung", + "custom_integrations": "Personnaliséiert Integratiounen", + "sla_guarantee": "SLA Garantie", + "dedicated_support": "Dedizéierte Kontobetreier" + }, + "addons": { + "title": "Erweidert Är Plattform", + "subtitle": "Füügt Är Mark, professionell Email a verbessert Sécherheet derbäi.", + "per_year": "/Joer", + "per_month": "/Mount", + "custom_domain": "Eegen Domain", + "custom_domain_desc": "Benotzt Är eegen Domain (mengdomain.lu)", + "premium_ssl": "Premium SSL", + "premium_ssl_desc": "EV Zertifikat fir Vertrauensbadgen", + "email_package": "Email Package", + "email_package_desc": "Professionell Email Adressen" + }, + "find_shop": { + "title": "Fannt Äre Letzshop Buttek", + "subtitle": "Verkaaft Dir schonn op Letzshop? Gitt Är Buttek URL an fir unzefänken.", + "placeholder": "Gitt Är Letzshop URL an (z.B. letzshop.lu/vendors/mäi-buttek)", + "button": "Mäi Buttek fannen", + "claim_shop": "Dëse Buttek reklaméieren", + "already_claimed": "Scho reklaméiert", + "no_account": "Kee Letzshop Kont?", + "signup_letzshop": "Registréiert Iech éischt bei Letzshop", + "then_connect": ", dann kommt zréck fir Äre Buttek ze verbannen.", + "search_placeholder": "Letzshop URL oder Butteknumm aginn...", + "search_button": "Sichen", + "examples": "Beispiller:", + "claim_button": "Dëse Buttek reklaméieren a gratis testen", + "not_found": "Mir konnten keen Letzshop Buttek mat dëser URL fannen. Iwwerpréift w.e.g. a probéiert nach eng Kéier.", + "or_signup": "Oder registréiert Iech ouni Letzshop Verbindung", + "need_help": "Braucht Dir Hëllef?", + "no_account_yet": "Dir hutt nach keen Letzshop Kont? Keen Problem!", + "create_letzshop": "Letzshop Kont erstellen", + "signup_without": "Ouni Letzshop registréieren", + "looking_up": "Sich Äre Buttek...", + "found": "Fonnt:", + "claimed_badge": "Scho reklaméiert" + }, + "signup": { + "step_plan": "Plang wielen", + "step_shop": "Buttek reklaméieren", + "step_account": "Kont", + "step_payment": "Bezuelung", + "choose_plan": "Wielt Äre Plang", + "save_percent": "Spuert {percent}%", + "trial_info": "Mir sammelen Är Bezuelungsinformatiounen, awer Dir gitt eréischt nom Enn vun der Testperiod belaaschtt.", + "connect_shop": "Verbannt Äre Letzshop Buttek", + "connect_optional": "Optional: Verlinkt Äre Letzshop Kont fir Bestellungen automatesch ze synchroniséieren.", + "connect_continue": "Verbannen a weider", + "skip_step": "Dëse Schrëtt iwwersprangen", + "create_account": "Erstellt Äre Kont", + "first_name": "Virnumm", + "last_name": "Numm", + "company_name": "Firmennumm", + "email": "Email", + "password": "Passwuert", + "password_hint": "Mindestens 8 Zeechen", + "continue": "Weider", + "continue_payment": "Weider zur Bezuelung", + "back": "Zréck", + "add_payment": "Bezuelungsmethod derbäisetzen", + "no_charge_note": "Dir gitt eréischt nom Enn vun Ärer {trial_days}-Deeg Testperiod belaaschtt.", + "processing": "Veraarbechtung...", + "start_trial": "Gratis Testversioun starten", + "creating_account": "Erstellt Äre Kont..." + }, + "success": { + "title": "Wëllkomm bei Wizamart!", + "subtitle": "Äre Kont gouf erstallt an Är {trial_days}-Deeg gratis Testversioun huet ugefaang.", + "what_next": "Wat kënnt duerno?", + "step_connect": "Letzshop verbannen:", + "step_connect_desc": "Füügt Äre API Schlëssel derbäi fir Bestellungen automatesch ze synchroniséieren.", + "step_invoicing": "Rechnungsstellung astellen:", + "step_invoicing_desc": "Konfiguréiert Är Rechnungsastellungen fir Lëtzebuerger Konformitéit.", + "step_products": "Produkter importéieren:", + "step_products_desc": "Synchroniséiert Äre Produktkatalog vu Letzshop.", + "go_to_dashboard": "Zum Dashboard", + "login_dashboard": "Am Dashboard umellen", + "need_help": "Braucht Dir Hëllef beim Ufänken?", + "contact_support": "Kontaktéiert eist Support Team" }, "cta": { - "title": "Call-to-Action-Sektioun", - "main_title": "Titel", - "subtitle": "Ënnertitel", - "buttons": "Knäpp", - "add_button": "Knapp derbäisetzen" + "title": "Prett fir Är Bestellungen ze optiméieren?", + "subtitle": "Schléisst Iech Letzshop Händler un déi Wizamart fir hir Bestellungsverwaltung vertrauen. Fänkt haut Är {trial_days}-Deeg gratis Testversioun un.", + "button": "Gratis Testen" + }, + "footer": { + "tagline": "Liichtt OMS fir Letzshop Verkeefer. Verwaltt Bestellungen, Lager an Rechnungen.", + "quick_links": "Séier Linken", + "platform": "Plattform", + "contact": "Kontakt", + "copyright": "© {year} Wizamart. Gemaach fir de lëtzebuergeschen E-Commerce.", + "privacy": "Dateschutzrichtlinn", + "terms": "Notzungsbedéngungen", + "about": "Iwwer eis", + "faq": "FAQ", + "contact_us": "Kontaktéiert eis" + }, + "modern": { + "badge_integration": "Offiziell Integratioun", + "badge_connect": "An 2 Minutten verbannen", + "hero_title_1": "Gemaach fir de lëtzebuergeschen E-Commerce", + "hero_title_2": "De Back-Office dee Letzshop Iech net gëtt", + "hero_subtitle": "Synchroniséiert Bestellungen, verwaltt Lager, erstellt Rechnunge mat der korrekter TVA a besëtzt Är Clientsdaten. Alles un engem Plaz.", + "cta_trial": "{trial_days}-Deeg gratis testen", + "cta_how": "Kuckt wéi et funktionéiert", + "hero_note": "Keng Kreditkaart néideg. Setup an 5 Minutten. Ëmmer kënnegen.", + "pain_title": "Kënnt Iech dat bekannt vir?", + "pain_subtitle": "Dat sinn d'deeglech Frustratioune vu Letzshop Verkeefer", + "pain_manual": "Manuell Bestellungsagab", + "pain_manual_desc": "Bestellunge vu Letzshop an Tabelle kopéieren. All. Eenzelen. Dag.", + "pain_inventory": "Lager Chaos", + "pain_inventory_desc": "De Stock an Letzshop stëmmt net mat der Realitéit iwwereneen. Iwwerverkeef passéieren.", + "pain_vat": "Falsch TVA Rechnungen", + "pain_vat_desc": "EU Cliente brauchen déi korrekt TVA. Äre Comptabel beschwéiert sech.", + "pain_customers": "Verluer Clienten", + "pain_customers_desc": "Letzshop besëtzt Är Clientsdaten. Dir kënnt se net retargeten oder Loyalitéit opbauen.", + "how_title": "Wéi et funktionéiert", + "how_subtitle": "Vum Chaos zur Kontroll an 4 Schrëtt", + "how_step1": "Letzshop verbannen", + "how_step1_desc": "Gitt Är Letzshop API Zougangsdaten an. An 2 Minutte fäerdeg, keng technesch Kenntnisser néideg.", + "how_step2": "Bestellunge kommen eran", + "how_step2_desc": "Bestellunge ginn automatesch synchroniséiert. Confirméiert an Tracking direkt vu Wizamart derbäisetzen.", + "how_step3": "Rechnunge generéieren", + "how_step3_desc": "Ee Klick fir konform PDF Rechnunge mat korrekter TVA fir all EU Land ze erstellen.", + "how_step4": "Äert Geschäft ausbauen", + "how_step4_desc": "Exportéiert Clientë fir Marketing. Verfolgt Lagerstänn. Konzentréiert Iech op de Verkaf, net op Tabellen.", + "features_title": "Alles wat e Letzshop Verkeefer brauch", + "features_subtitle": "D'operativ Tools déi Letzshop net bitt", + "cta_final_title": "Prett fir d'Kontroll iwwer Äert Letzshop Geschäft ze iwwerhuelen?", + "cta_final_subtitle": "Schléisst Iech lëtzebuerger Händler un déi opgehalen hunn géint Tabellen ze kämpfen an ugefaang hunn hiert Geschäft auszbauen.", + "cta_final_note": "Keng Kreditkaart néideg. Setup an 5 Minutten. Voll Professional Fonctiounen während der Testperiod." } - }, - "media": { - "title": "Mediebibliothéik", - "upload": "Eroplueden", - "upload_file": "Fichier eroplueden", - "delete": "Läschen", - "empty": "Keng Mediefichieren", - "upload_first": "Luet Äre éischte Fichier erop" - }, - "themes": { - "title": "Händler-Themen", - "subtitle": "Verwaltet Händler-Theme-Personnalisatiounen" - }, - "actions": { - "save": "Späicheren", - "saving": "Späicheren...", - "update": "Säit aktualiséieren", - "create": "Säit erstellen", - "cancel": "Ofbriechen", - "back_to_list": "Zréck op d'Lëscht", - "preview": "Virschau", - "revert_to_default": "Op Standard zrécksetzen" - }, - "messages": { - "created": "Säit erfollegräich erstallt", - "updated": "Säit erfollegräich aktualiséiert", - "deleted": "Säit erfollegräich geläscht", - "reverted": "Op Standardsäit zréckgesat", - "error_loading": "Feeler beim Lueden vun der Säit", - "error_saving": "Feeler beim Späichere vun der Säit", - "confirm_delete": "Sidd Dir sécher, datt Dir dës Säit läsche wëllt?" - }, - "filters": { - "all_pages": "All Säiten", - "all_platforms": "All Plattformen", - "search_placeholder": "Säite sichen..." } } diff --git a/app/modules/cms/models/__init__.py b/app/modules/cms/models/__init__.py index 79255a88..ba0744c3 100644 --- a/app/modules/cms/models/__init__.py +++ b/app/modules/cms/models/__init__.py @@ -2,19 +2,24 @@ """ CMS module database models. -This is the canonical location for CMS models. Module models are automatically -discovered and registered with SQLAlchemy's Base.metadata at startup. +This is the canonical location for CMS models including: +- ContentPage: CMS pages (marketing, vendor default pages) +- MediaFile: Vendor media library +- VendorTheme: Vendor storefront theme configuration Usage: - from app.modules.cms.models import ContentPage + from app.modules.cms.models import ContentPage, MediaFile, VendorTheme -For media models: - from models.database.media import MediaFile # Core media file storage - from app.modules.catalog.models import ProductMedia # Product-media associations +For product-media associations: + from app.modules.catalog.models import ProductMedia """ from app.modules.cms.models.content_page import ContentPage +from app.modules.cms.models.media import MediaFile +from app.modules.cms.models.vendor_theme import VendorTheme __all__ = [ "ContentPage", + "MediaFile", + "VendorTheme", ] diff --git a/models/database/media.py b/app/modules/cms/models/media.py similarity index 94% rename from models/database/media.py rename to app/modules/cms/models/media.py index b0ce8eaa..85f8f2dd 100644 --- a/models/database/media.py +++ b/app/modules/cms/models/media.py @@ -1,4 +1,4 @@ -# models/database/media.py +# app/modules/cms/models/media.py """ CORE media file model for vendor media library. @@ -121,7 +121,4 @@ class MediaFile(Base, TimestampMixin): return self.media_type == "document" -# Re-export ProductMedia from its canonical location for backwards compatibility -from app.modules.catalog.models import ProductMedia # noqa: E402, F401 - -__all__ = ["MediaFile", "ProductMedia"] +__all__ = ["MediaFile"] diff --git a/models/database/vendor_theme.py b/app/modules/cms/models/vendor_theme.py similarity index 98% rename from models/database/vendor_theme.py rename to app/modules/cms/models/vendor_theme.py index 1a0dea49..1b89f03b 100644 --- a/models/database/vendor_theme.py +++ b/app/modules/cms/models/vendor_theme.py @@ -1,4 +1,4 @@ -# models/database/vendor_theme.py +# app/modules/cms/models/vendor_theme.py """ Vendor Theme Configuration Model Allows each vendor to customize their shop's appearance @@ -134,3 +134,6 @@ class VendorTheme(Base, TimestampMixin): "custom_css": self.custom_css, "css_variables": self.css_variables, } + + +__all__ = ["VendorTheme"] diff --git a/app/modules/cms/routes/api/admin_content_pages.py b/app/modules/cms/routes/api/admin_content_pages.py index 658940fb..c8033a20 100644 --- a/app/modules/cms/routes/api/admin_content_pages.py +++ b/app/modules/cms/routes/api/admin_content_pages.py @@ -23,7 +23,7 @@ from app.modules.cms.schemas import ( SectionUpdateResponse, ) from app.modules.cms.services import content_page_service -from models.database.user import User +from app.modules.tenancy.models import User admin_content_pages_router = APIRouter(prefix="/content-pages") logger = logging.getLogger(__name__) diff --git a/app/modules/cms/routes/api/admin_images.py b/app/modules/cms/routes/api/admin_images.py index a8f11f63..f7565753 100644 --- a/app/modules/cms/routes/api/admin_images.py +++ b/app/modules/cms/routes/api/admin_images.py @@ -15,7 +15,7 @@ from fastapi import APIRouter, Depends, File, Form, UploadFile from app.api.deps import get_current_admin_api from app.modules.core.services.image_service import image_service from models.schema.auth import UserContext -from models.schema.image import ( +from app.modules.cms.schemas.image import ( ImageDeleteResponse, ImageStorageStats, ImageUploadResponse, diff --git a/app/modules/cms/routes/api/admin_media.py b/app/modules/cms/routes/api/admin_media.py index d3de5892..43407afa 100644 --- a/app/modules/cms/routes/api/admin_media.py +++ b/app/modules/cms/routes/api/admin_media.py @@ -14,7 +14,7 @@ from app.api.deps import get_current_admin_api from app.core.database import get_db from app.modules.cms.services.media_service import media_service from models.schema.auth import UserContext -from models.schema.media import ( +from app.modules.cms.schemas.media import ( MediaDetailResponse, MediaItemResponse, MediaListResponse, diff --git a/app/modules/cms/routes/api/admin_vendor_themes.py b/app/modules/cms/routes/api/admin_vendor_themes.py index 9888f07e..c96e6d3b 100644 --- a/app/modules/cms/routes/api/admin_vendor_themes.py +++ b/app/modules/cms/routes/api/admin_vendor_themes.py @@ -20,7 +20,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, get_db from app.modules.cms.services.vendor_theme_service import vendor_theme_service from models.schema.auth import UserContext -from models.schema.vendor_theme import ( +from app.modules.cms.schemas.vendor_theme import ( ThemeDeleteResponse, ThemePresetListResponse, ThemePresetResponse, diff --git a/app/modules/cms/routes/api/vendor_content_pages.py b/app/modules/cms/routes/api/vendor_content_pages.py index 141e8984..0d6d3224 100644 --- a/app/modules/cms/routes/api/vendor_content_pages.py +++ b/app/modules/cms/routes/api/vendor_content_pages.py @@ -26,7 +26,7 @@ from app.modules.cms.schemas import ( ) from app.modules.cms.services import content_page_service from app.modules.tenancy.services.vendor_service import VendorService # noqa: MOD-004 - shared platform service -from models.database.user import User +from app.modules.tenancy.models import User vendor_service = VendorService() diff --git a/app/modules/cms/routes/api/vendor_media.py b/app/modules/cms/routes/api/vendor_media.py index c0ba1d22..a216afb8 100644 --- a/app/modules/cms/routes/api/vendor_media.py +++ b/app/modules/cms/routes/api/vendor_media.py @@ -16,7 +16,7 @@ from app.core.database import get_db from app.modules.cms.exceptions import MediaOptimizationException from app.modules.cms.services.media_service import media_service from models.schema.auth import UserContext -from models.schema.media import ( +from app.modules.cms.schemas.media import ( MediaDetailResponse, MediaItemResponse, MediaListResponse, diff --git a/app/modules/cms/routes/pages/admin.py b/app/modules/cms/routes/pages/admin.py index 93817a47..21df0c43 100644 --- a/app/modules/cms/routes/pages/admin.py +++ b/app/modules/cms/routes/pages/admin.py @@ -11,8 +11,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.templates_config import templates -from models.database.admin_menu_config import FrontendType -from models.database.user import User +from app.modules.enums import FrontendType +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/cms/routes/pages/vendor.py b/app/modules/cms/routes/pages/vendor.py index 325b7cbb..2f6c1fc8 100644 --- a/app/modules/cms/routes/pages/vendor.py +++ b/app/modules/cms/routes/pages/vendor.py @@ -15,8 +15,8 @@ from app.api.deps import get_current_vendor_from_cookie_or_header, get_db from app.modules.cms.services import content_page_service from app.modules.core.services.platform_settings_service import platform_settings_service # noqa: MOD-004 - shared platform service from app.templates_config import templates -from models.database.user import User -from models.database.vendor import Vendor +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/cms/schemas/__init__.py b/app/modules/cms/schemas/__init__.py index 4b139dd4..bd867840 100644 --- a/app/modules/cms/schemas/__init__.py +++ b/app/modules/cms/schemas/__init__.py @@ -35,6 +35,44 @@ from app.modules.cms.schemas.homepage_sections import ( HomepageSectionsResponse, ) +# Media schemas +from app.modules.cms.schemas.media import ( + FailedFileInfo, + MediaDetailResponse, + MediaItemResponse, + MediaListResponse, + MediaMetadataUpdate, + MediaUploadResponse, + MediaUsageResponse, + MessageResponse, + MultipleUploadResponse, + OptimizationResultResponse, + ProductUsageInfo, + UploadedFileInfo, +) + +# Image schemas +from app.modules.cms.schemas.image import ( + ImageDeleteResponse, + ImageStorageStats, + ImageUploadResponse, + ImageUrls, +) + +# Theme schemas +from app.modules.cms.schemas.vendor_theme import ( + ThemeDeleteResponse, + ThemePresetListResponse, + ThemePresetPreview, + ThemePresetResponse, + VendorThemeBranding, + VendorThemeColors, + VendorThemeFonts, + VendorThemeLayout, + VendorThemeResponse, + VendorThemeUpdate, +) + __all__ = [ # Content Page - Admin "ContentPageCreate", @@ -60,4 +98,33 @@ __all__ = [ "HomepageSections", "SectionUpdateRequest", "HomepageSectionsResponse", + # Media + "FailedFileInfo", + "MediaDetailResponse", + "MediaItemResponse", + "MediaListResponse", + "MediaMetadataUpdate", + "MediaUploadResponse", + "MediaUsageResponse", + "MessageResponse", + "MultipleUploadResponse", + "OptimizationResultResponse", + "ProductUsageInfo", + "UploadedFileInfo", + # Image + "ImageDeleteResponse", + "ImageStorageStats", + "ImageUploadResponse", + "ImageUrls", + # Theme + "ThemeDeleteResponse", + "ThemePresetListResponse", + "ThemePresetPreview", + "ThemePresetResponse", + "VendorThemeBranding", + "VendorThemeColors", + "VendorThemeFonts", + "VendorThemeLayout", + "VendorThemeResponse", + "VendorThemeUpdate", ] diff --git a/models/schema/image.py b/app/modules/cms/schemas/image.py similarity index 96% rename from models/schema/image.py rename to app/modules/cms/schemas/image.py index c0c61d27..0813b380 100644 --- a/models/schema/image.py +++ b/app/modules/cms/schemas/image.py @@ -1,4 +1,4 @@ -# models/schema/image.py +# app/modules/cms/schemas/image.py """ Pydantic schemas for image operations. """ diff --git a/models/schema/media.py b/app/modules/cms/schemas/media.py similarity index 99% rename from models/schema/media.py rename to app/modules/cms/schemas/media.py index 0f106bcb..f18e5512 100644 --- a/models/schema/media.py +++ b/app/modules/cms/schemas/media.py @@ -1,4 +1,4 @@ -# models/schema/media.py +# app/modules/cms/schemas/media.py """ Media/file management Pydantic schemas for API validation and responses. diff --git a/models/schema/vendor_theme.py b/app/modules/cms/schemas/vendor_theme.py similarity index 98% rename from models/schema/vendor_theme.py rename to app/modules/cms/schemas/vendor_theme.py index 42f8cae9..5ce1a0dc 100644 --- a/models/schema/vendor_theme.py +++ b/app/modules/cms/schemas/vendor_theme.py @@ -1,4 +1,4 @@ -# models/schema/vendor_theme.py +# app/modules/cms/schemas/vendor_theme.py """ Pydantic schemas for vendor theme operations. """ diff --git a/app/modules/cms/services/media_service.py b/app/modules/cms/services/media_service.py index 6227e4b2..379ac49c 100644 --- a/app/modules/cms/services/media_service.py +++ b/app/modules/cms/services/media_service.py @@ -27,7 +27,7 @@ from app.modules.cms.exceptions import ( UnsupportedMediaTypeException, MediaFileTooLargeException, ) -from models.database.media import MediaFile +from app.modules.cms.models import MediaFile from app.modules.catalog.models import ProductMedia logger = logging.getLogger(__name__) diff --git a/app/modules/cms/services/vendor_email_settings_service.py b/app/modules/cms/services/vendor_email_settings_service.py index aa1a6c5b..e336059c 100644 --- a/app/modules/cms/services/vendor_email_settings_service.py +++ b/app/modules/cms/services/vendor_email_settings_service.py @@ -24,14 +24,13 @@ from app.exceptions import ( ValidationException, ExternalServiceException, ) -from models.database import ( - Vendor, +from app.modules.tenancy.models import Vendor +from app.modules.messaging.models import ( VendorEmailSettings, EmailProvider, PREMIUM_EMAIL_PROVIDERS, - VendorSubscription, - TierCode, ) +from app.modules.billing.models import VendorSubscription, TierCode logger = logging.getLogger(__name__) diff --git a/app/modules/cms/services/vendor_theme_service.py b/app/modules/cms/services/vendor_theme_service.py index e26fd171..157c6963 100644 --- a/app/modules/cms/services/vendor_theme_service.py +++ b/app/modules/cms/services/vendor_theme_service.py @@ -26,9 +26,9 @@ from app.modules.cms.exceptions import ( ThemeValidationException, VendorThemeNotFoundException, ) -from models.database.vendor import Vendor -from models.database.vendor_theme import VendorTheme -from models.schema.vendor_theme import ThemePresetPreview, VendorThemeUpdate +from app.modules.tenancy.models import Vendor +from app.modules.cms.models import VendorTheme +from app.modules.cms.schemas.vendor_theme import ThemePresetPreview, VendorThemeUpdate logger = logging.getLogger(__name__) diff --git a/app/modules/cms/static/admin/js/content-pages.js b/app/modules/cms/static/admin/js/content-pages.js index 1599d3da..3a58f4ba 100644 --- a/app/modules/cms/static/admin/js/content-pages.js +++ b/app/modules/cms/static/admin/js/content-pages.js @@ -28,6 +28,9 @@ function contentPagesManager() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('cms'); + contentPagesLog.info('=== CONTENT PAGES MANAGER INITIALIZING ==='); // Prevent multiple initializations @@ -235,7 +238,7 @@ function contentPagesManager() { } catch (err) { contentPagesLog.error('Error deleting page:', err); - Utils.showToast(`Failed to delete page: ${err.message}`, 'error'); + Utils.showToast(I18n.t('cms.messages.failed_to_delete_page', { error: err.message }), 'error'); } }, diff --git a/app/modules/cms/templates/cms/public/homepage-wizamart.html b/app/modules/cms/templates/cms/public/homepage-wizamart.html index c1347618..1a5075da 100644 --- a/app/modules/cms/templates/cms/public/homepage-wizamart.html +++ b/app/modules/cms/templates/cms/public/homepage-wizamart.html @@ -20,24 +20,24 @@ - {{ _("platform.hero.badge", trial_days=trial_days) }} + {{ _("cms.platform.hero.badge", trial_days=trial_days) }} {# Headline #}- {{ _("platform.hero.subtitle") }} + {{ _("cms.platform.hero.subtitle") }}
{# CTA Buttons #} @@ -68,19 +68,19 @@ {# Section Header #}- {{ _("platform.pricing.subtitle", trial_days=trial_days) }} + {{ _("cms.platform.pricing.subtitle", trial_days=trial_days) }}
{# Billing Toggle #}- {{ _("platform.addons.subtitle") }} + {{ _("cms.platform.addons.subtitle") }}
- {{ _("platform.find_shop.subtitle") }} + {{ _("cms.platform.find_shop.subtitle") }}
- {{ _("platform.find_shop.no_account") }} {{ _("platform.find_shop.signup_letzshop") }}{{ _("platform.find_shop.then_connect") }} + {{ _("cms.platform.find_shop.no_account") }} {{ _("cms.platform.find_shop.signup_letzshop") }}{{ _("cms.platform.find_shop.then_connect") }}
@@ -374,14 +374,14 @@- {{ _("platform.cta.subtitle", trial_days=trial_days) }} + {{ _("cms.platform.cta.subtitle", trial_days=trial_days) }}
- {{ _("platform.cta.button") }} + {{ _("cms.platform.cta.button") }} diff --git a/app/modules/core/definition.py b/app/modules/core/definition.py index f34815ca..3b1694f6 100644 --- a/app/modules/core/definition.py +++ b/app/modules/core/definition.py @@ -6,8 +6,8 @@ Dashboard, settings, and profile management. Required for basic operation - cannot be disabled. """ -from app.modules.base import ModuleDefinition -from models.database.admin_menu_config import FrontendType +from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition +from app.modules.enums import FrontendType core_module = ModuleDefinition( code="core", @@ -21,6 +21,7 @@ core_module = ModuleDefinition( "settings", "profile", ], + # Legacy menu_items (IDs only) menu_items={ FrontendType.ADMIN: [ "dashboard", @@ -35,6 +36,95 @@ core_module = ModuleDefinition( "email-templates", ], }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="main", + label_key=None, # No header for main section + icon=None, + order=0, + is_collapsible=False, + items=[ + MenuItemDefinition( + id="dashboard", + label_key="core.menu.dashboard", + icon="home", + route="/admin/dashboard", + order=10, + is_mandatory=True, + ), + ], + ), + MenuSectionDefinition( + id="settings", + label_key="core.menu.platform_settings", + icon="cog", + order=900, + items=[ + MenuItemDefinition( + id="settings", + label_key="core.menu.general", + icon="cog", + route="/admin/settings", + order=10, + is_mandatory=True, + ), + MenuItemDefinition( + id="my-menu", + label_key="core.menu.my_menu", + icon="view-grid", + route="/admin/my-menu", + order=30, + is_mandatory=True, + is_super_admin_only=True, + ), + ], + ), + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="main", + label_key=None, + icon=None, + order=0, + is_collapsible=False, + items=[ + MenuItemDefinition( + id="dashboard", + label_key="core.menu.dashboard", + icon="home", + route="/vendor/{vendor_code}/dashboard", + order=10, + is_mandatory=True, + ), + ], + ), + MenuSectionDefinition( + id="account", + label_key="core.menu.account_settings", + icon="user", + order=900, + items=[ + MenuItemDefinition( + id="profile", + label_key="core.menu.profile", + icon="user", + route="/vendor/{vendor_code}/profile", + order=10, + ), + MenuItemDefinition( + id="settings", + label_key="core.menu.settings", + icon="cog", + route="/vendor/{vendor_code}/settings", + order=20, + is_mandatory=True, + ), + ], + ), + ], + }, ) __all__ = ["core_module"] diff --git a/app/modules/core/exceptions.py b/app/modules/core/exceptions.py new file mode 100644 index 00000000..effc2aff --- /dev/null +++ b/app/modules/core/exceptions.py @@ -0,0 +1,34 @@ +# app/modules/core/exceptions.py +"""Core module exceptions. + +Exceptions for core platform functionality including: +- Menu configuration +- Dashboard operations +- Settings management +""" + +from app.exceptions import WizamartException + + +class CoreException(WizamartException): + """Base exception for core module.""" + + pass + + +class MenuConfigurationError(CoreException): + """Error in menu configuration.""" + + pass + + +class SettingsError(CoreException): + """Error in platform settings.""" + + pass + + +class DashboardError(CoreException): + """Error in dashboard operations.""" + + pass diff --git a/app/modules/core/locales/de.json b/app/modules/core/locales/de.json new file mode 100644 index 00000000..eeb12ee0 --- /dev/null +++ b/app/modules/core/locales/de.json @@ -0,0 +1,71 @@ +{ + "dashboard": { + "title": "Dashboard", + "welcome": "Willkommen zurück", + "overview": "Übersicht", + "quick_stats": "Schnellstatistiken", + "recent_activity": "Letzte Aktivitäten", + "total_products": "Produkte gesamt", + "total_orders": "Bestellungen gesamt", + "total_customers": "Kunden gesamt", + "total_revenue": "Gesamtumsatz", + "active_products": "Aktive Produkte", + "pending_orders": "Ausstehende Bestellungen", + "new_customers": "Neue Kunden", + "today": "Heute", + "this_week": "Diese Woche", + "this_month": "Dieser Monat", + "this_year": "Dieses Jahr", + "error_loading": "Fehler beim Laden des Dashboards", + "no_data": "Keine Daten verfügbar" + }, + "settings": { + "title": "Einstellungen", + "general": "Allgemein", + "store": "Shop", + "store_name": "Shop-Name", + "store_description": "Shop-Beschreibung", + "contact_email": "Kontakt-E-Mail", + "contact_phone": "Kontakttelefon", + "business_address": "Geschäftsadresse", + "tax_number": "Steuernummer", + "currency": "Währung", + "timezone": "Zeitzone", + "language": "Sprache", + "language_settings": "Spracheinstellungen", + "default_language": "Standardsprache", + "dashboard_language": "Dashboard-Sprache", + "storefront_language": "Shop-Sprache", + "enabled_languages": "Aktivierte Sprachen", + "notifications": "Benachrichtigungen", + "email_notifications": "E-Mail-Benachrichtigungen", + "integrations": "Integrationen", + "api_keys": "API-Schlüssel", + "webhooks": "Webhooks", + "save_settings": "Einstellungen speichern", + "settings_saved": "Einstellungen erfolgreich gespeichert" + }, + "profile": { + "title": "Profil", + "my_profile": "Mein Profil", + "edit_profile": "Profil bearbeiten", + "personal_info": "Persönliche Informationen", + "first_name": "Vorname", + "last_name": "Nachname", + "email": "E-Mail", + "phone": "Telefon", + "avatar": "Profilbild", + "change_avatar": "Profilbild ändern", + "security": "Sicherheit", + "two_factor": "Zwei-Faktor-Authentifizierung", + "sessions": "Aktive Sitzungen", + "preferences": "Präferenzen", + "language_preference": "Sprachpräferenz", + "save_profile": "Profil speichern", + "profile_updated": "Profil erfolgreich aktualisiert" + }, + "messages": { + "failed_to_load_dashboard_data": "Failed to load dashboard data", + "dashboard_refreshed": "Dashboard refreshed" + } +} diff --git a/app/modules/core/locales/fr.json b/app/modules/core/locales/fr.json new file mode 100644 index 00000000..3301d5ad --- /dev/null +++ b/app/modules/core/locales/fr.json @@ -0,0 +1,71 @@ +{ + "dashboard": { + "title": "Tableau de bord", + "welcome": "Bienvenue", + "overview": "Vue d'ensemble", + "quick_stats": "Statistiques rapides", + "recent_activity": "Activité récente", + "total_products": "Total des produits", + "total_orders": "Total des commandes", + "total_customers": "Total des clients", + "total_revenue": "Chiffre d'affaires total", + "active_products": "Produits actifs", + "pending_orders": "Commandes en attente", + "new_customers": "Nouveaux clients", + "today": "Aujourd'hui", + "this_week": "Cette semaine", + "this_month": "Ce mois", + "this_year": "Cette année", + "error_loading": "Erreur lors du chargement du tableau de bord", + "no_data": "Aucune donnée disponible" + }, + "settings": { + "title": "Paramètres", + "general": "Général", + "store": "Boutique", + "store_name": "Nom de la boutique", + "store_description": "Description de la boutique", + "contact_email": "E-mail de contact", + "contact_phone": "Téléphone de contact", + "business_address": "Adresse professionnelle", + "tax_number": "Numéro de TVA", + "currency": "Devise", + "timezone": "Fuseau horaire", + "language": "Langue", + "language_settings": "Paramètres de langue", + "default_language": "Langue par défaut", + "dashboard_language": "Langue du tableau de bord", + "storefront_language": "Langue de la boutique", + "enabled_languages": "Langues activées", + "notifications": "Notifications", + "email_notifications": "Notifications par e-mail", + "integrations": "Intégrations", + "api_keys": "Clés API", + "webhooks": "Webhooks", + "save_settings": "Enregistrer les paramètres", + "settings_saved": "Paramètres enregistrés avec succès" + }, + "profile": { + "title": "Profil", + "my_profile": "Mon profil", + "edit_profile": "Modifier le profil", + "personal_info": "Informations personnelles", + "first_name": "Prénom", + "last_name": "Nom", + "email": "E-mail", + "phone": "Téléphone", + "avatar": "Avatar", + "change_avatar": "Changer l'avatar", + "security": "Sécurité", + "two_factor": "Authentification à deux facteurs", + "sessions": "Sessions actives", + "preferences": "Préférences", + "language_preference": "Préférence de langue", + "save_profile": "Enregistrer le profil", + "profile_updated": "Profil mis à jour avec succès" + }, + "messages": { + "failed_to_load_dashboard_data": "Failed to load dashboard data", + "dashboard_refreshed": "Dashboard refreshed" + } +} diff --git a/app/modules/core/locales/lb.json b/app/modules/core/locales/lb.json new file mode 100644 index 00000000..7a93ef90 --- /dev/null +++ b/app/modules/core/locales/lb.json @@ -0,0 +1,71 @@ +{ + "dashboard": { + "title": "Dashboard", + "welcome": "Wëllkomm zréck", + "overview": "Iwwersiicht", + "quick_stats": "Séier Statistiken", + "recent_activity": "Rezent Aktivitéit", + "total_products": "Produkter insgesamt", + "total_orders": "Bestellungen insgesamt", + "total_customers": "Clienten insgesamt", + "total_revenue": "Ëmsaz insgesamt", + "active_products": "Aktiv Produkter", + "pending_orders": "Aussteesend Bestellungen", + "new_customers": "Nei Clienten", + "today": "Haut", + "this_week": "Dës Woch", + "this_month": "Dëse Mount", + "this_year": "Dëst Joer", + "error_loading": "Feeler beim Lueden vum Dashboard", + "no_data": "Keng Donnéeën disponibel" + }, + "settings": { + "title": "Astellungen", + "general": "Allgemeng", + "store": "Buttek", + "store_name": "Butteknumm", + "store_description": "Buttekbeschreiwung", + "contact_email": "Kontakt E-Mail", + "contact_phone": "Kontakt Telefon", + "business_address": "Geschäftsadress", + "tax_number": "Steiernummer", + "currency": "Wärung", + "timezone": "Zäitzon", + "language": "Sprooch", + "language_settings": "Sproochastellungen", + "default_language": "Standard Sprooch", + "dashboard_language": "Dashboard Sprooch", + "storefront_language": "Buttek Sprooch", + "enabled_languages": "Aktivéiert Sproochen", + "notifications": "Notifikatiounen", + "email_notifications": "E-Mail Notifikatiounen", + "integrations": "Integratiounen", + "api_keys": "API Schlësselen", + "webhooks": "Webhooks", + "save_settings": "Astellunge späicheren", + "settings_saved": "Astellungen erfollegräich gespäichert" + }, + "profile": { + "title": "Profil", + "my_profile": "Mäi Profil", + "edit_profile": "Profil änneren", + "personal_info": "Perséinlech Informatiounen", + "first_name": "Virnumm", + "last_name": "Nonumm", + "email": "E-Mail", + "phone": "Telefon", + "avatar": "Avatar", + "change_avatar": "Avatar änneren", + "security": "Sécherheet", + "two_factor": "Zwee-Faktor Authentifikatioun", + "sessions": "Aktiv Sessiounen", + "preferences": "Astellungen", + "language_preference": "Sproochpräferenz", + "save_profile": "Profil späicheren", + "profile_updated": "Profil erfollegräich aktualiséiert" + }, + "messages": { + "failed_to_load_dashboard_data": "Failed to load dashboard data", + "dashboard_refreshed": "Dashboard refreshed" + } +} diff --git a/app/modules/core/models/__init__.py b/app/modules/core/models/__init__.py new file mode 100644 index 00000000..e13278c2 --- /dev/null +++ b/app/modules/core/models/__init__.py @@ -0,0 +1,18 @@ +# app/modules/core/models/__init__.py +""" +Core module database models. + +This is the canonical location for core module models. +""" + +from app.modules.core.models.admin_menu_config import ( + AdminMenuConfig, + FrontendType, + MANDATORY_MENU_ITEMS, +) + +__all__ = [ + "AdminMenuConfig", + "FrontendType", + "MANDATORY_MENU_ITEMS", +] diff --git a/models/database/admin_menu_config.py b/app/modules/core/models/admin_menu_config.py similarity index 96% rename from models/database/admin_menu_config.py rename to app/modules/core/models/admin_menu_config.py index 1c65ea8d..c7324a9f 100644 --- a/models/database/admin_menu_config.py +++ b/app/modules/core/models/admin_menu_config.py @@ -1,4 +1,4 @@ -# models/database/admin_menu_config.py +# app/modules/core/models/admin_menu_config.py """ Menu visibility configuration for admin and vendor frontends. @@ -36,9 +36,6 @@ from app.core.database import Base from models.database.base import TimestampMixin # Import FrontendType and MANDATORY_MENU_ITEMS from the central location -# and re-export for backward compatibility with existing imports. -# These were moved to app.modules.enums to break a circular import: -# app.modules.base -> models.database -> model discovery -> module definitions -> app.modules.base from app.modules.enums import FrontendType, MANDATORY_MENU_ITEMS @@ -221,3 +218,6 @@ class AdminMenuConfig(Base, TimestampMixin): f"menu_item_id='{self.menu_item_id}', " f"is_visible={self.is_visible})>" ) + + +__all__ = ["AdminMenuConfig", "FrontendType", "MANDATORY_MENU_ITEMS"] diff --git a/app/modules/core/routes/api/admin.py b/app/modules/core/routes/api/admin.py index ef774df0..acecc1f3 100644 --- a/app/modules/core/routes/api/admin.py +++ b/app/modules/core/routes/api/admin.py @@ -5,15 +5,18 @@ Core module admin API routes. Aggregates all admin core routes: - /dashboard/* - Admin dashboard and statistics - /settings/* - Platform settings management +- /menu-config/* - Menu visibility configuration """ from fastapi import APIRouter from .admin_dashboard import admin_dashboard_router from .admin_settings import admin_settings_router +from .admin_menu_config import router as admin_menu_config_router admin_router = APIRouter() # Aggregate all core admin routes admin_router.include_router(admin_dashboard_router, tags=["admin-dashboard"]) admin_router.include_router(admin_settings_router, tags=["admin-settings"]) +admin_router.include_router(admin_menu_config_router, tags=["admin-menu-config"]) diff --git a/app/api/v1/admin/menu_config.py b/app/modules/core/routes/api/admin_menu_config.py similarity index 99% rename from app/api/v1/admin/menu_config.py rename to app/modules/core/routes/api/admin_menu_config.py index f7c48fa9..4cc6823b 100644 --- a/app/api/v1/admin/menu_config.py +++ b/app/modules/core/routes/api/admin_menu_config.py @@ -1,4 +1,4 @@ -# app/api/v1/admin/menu_config.py +# app/modules/core/routes/api/admin_menu_config.py """ Admin API endpoints for Platform Menu Configuration. @@ -29,7 +29,7 @@ from app.api.deps import ( ) from app.modules.core.services.menu_service import MenuItemConfig, menu_service from app.modules.tenancy.services.platform_service import platform_service -from models.database.admin_menu_config import FrontendType # noqa: API-007 - Enum for type safety +from app.modules.enums import FrontendType # noqa: API-007 - Enum for type safety from models.schema.auth import UserContext logger = logging.getLogger(__name__) diff --git a/app/modules/core/routes/api/admin_settings.py b/app/modules/core/routes/api/admin_settings.py index a7a6b537..eb21f8a2 100644 --- a/app/modules/core/routes/api/admin_settings.py +++ b/app/modules/core/routes/api/admin_settings.py @@ -23,7 +23,7 @@ from app.modules.tenancy.exceptions import ConfirmationRequiredException from app.modules.monitoring.services.admin_audit_service import admin_audit_service from app.modules.core.services.admin_settings_service import admin_settings_service from models.schema.auth import UserContext -from models.schema.admin import ( +from app.modules.tenancy.schemas.admin import ( AdminSettingCreate, AdminSettingDefaultResponse, AdminSettingListResponse, @@ -528,7 +528,7 @@ def update_email_settings( Settings are stored in the database and override .env values. Only non-null values are updated. """ - from models.schema.admin import AdminSettingCreate + from app.modules.tenancy.schemas.admin import AdminSettingCreate updated_keys = [] diff --git a/app/modules/core/routes/pages/admin.py b/app/modules/core/routes/pages/admin.py index 883e92ed..61c1113e 100644 --- a/app/modules/core/routes/pages/admin.py +++ b/app/modules/core/routes/pages/admin.py @@ -16,8 +16,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_admin_optional, get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context from app.templates_config import templates -from models.database.admin_menu_config import FrontendType -from models.database.user import User +from app.modules.enums import FrontendType +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/core/routes/pages/vendor.py b/app/modules/core/routes/pages/vendor.py index e1865a1d..27abc88f 100644 --- a/app/modules/core/routes/pages/vendor.py +++ b/app/modules/core/routes/pages/vendor.py @@ -14,7 +14,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_vendor_context from app.templates_config import templates -from models.database.user import User +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/core/schemas/__init__.py b/app/modules/core/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/core/services/__init__.py b/app/modules/core/services/__init__.py index 3c9e45dc..20cbbfbd 100644 --- a/app/modules/core/services/__init__.py +++ b/app/modules/core/services/__init__.py @@ -18,6 +18,12 @@ from app.modules.core.services.admin_settings_service import ( from app.modules.core.services.auth_service import AuthService, auth_service from app.modules.core.services.image_service import ImageService, image_service from app.modules.core.services.menu_service import MenuItemConfig, MenuService, menu_service +from app.modules.core.services.menu_discovery_service import ( + DiscoveredMenuItem, + DiscoveredMenuSection, + MenuDiscoveryService, + menu_discovery_service, +) from app.modules.core.services.platform_settings_service import ( PlatformSettingsService, platform_settings_service, @@ -34,10 +40,15 @@ __all__ = [ # Auth "AuthService", "auth_service", - # Menu + # Menu (legacy) "MenuService", "MenuItemConfig", "menu_service", + # Menu Discovery (module-driven) + "MenuDiscoveryService", + "DiscoveredMenuItem", + "DiscoveredMenuSection", + "menu_discovery_service", # Image "ImageService", "image_service", diff --git a/app/modules/core/services/admin_settings_service.py b/app/modules/core/services/admin_settings_service.py index 69e10c9d..46a077a8 100644 --- a/app/modules/core/services/admin_settings_service.py +++ b/app/modules/core/services/admin_settings_service.py @@ -21,8 +21,8 @@ from app.exceptions import ( ValidationException, ) from app.modules.tenancy.exceptions import AdminOperationException -from models.database.admin import AdminSetting -from models.schema.admin import ( +from app.modules.tenancy.models import AdminSetting +from app.modules.tenancy.schemas.admin import ( AdminSettingCreate, AdminSettingResponse, AdminSettingUpdate, diff --git a/app/modules/core/services/auth_service.py b/app/modules/core/services/auth_service.py index ccb7004a..675eb038 100644 --- a/app/modules/core/services/auth_service.py +++ b/app/modules/core/services/auth_service.py @@ -19,8 +19,8 @@ from sqlalchemy.orm import Session from app.modules.tenancy.exceptions import InvalidCredentialsException, UserNotActiveException from middleware.auth import AuthManager -from models.database.user import User -from models.database.vendor import Vendor, VendorUser +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Vendor, VendorUser from models.schema.auth import UserLogin logger = logging.getLogger(__name__) diff --git a/app/modules/core/services/menu_discovery_service.py b/app/modules/core/services/menu_discovery_service.py new file mode 100644 index 00000000..e3118e09 --- /dev/null +++ b/app/modules/core/services/menu_discovery_service.py @@ -0,0 +1,446 @@ +# app/modules/core/services/menu_discovery_service.py +""" +Menu Discovery Service - Discovers and aggregates menu items from all modules. + +This service implements the module-driven menu system where each module +defines its own menu items through MenuSectionDefinition and MenuItemDefinition +in its definition.py file. + +Key Features: +- Discovers menu definitions from all loaded modules +- Filters by module enablement (disabled modules = hidden menus) +- Respects user/platform visibility preferences (AdminMenuConfig) +- Supports permission-based filtering +- Enforces mandatory item visibility + +Usage: + from app.modules.core.services.menu_discovery_service import menu_discovery_service + + # Get complete menu for admin frontend + menu = menu_discovery_service.get_menu_for_frontend( + db, + FrontendType.ADMIN, + platform_id=1, + user=current_user + ) + + # Get flat list of all menu items for configuration UI + items = menu_discovery_service.get_all_menu_items(FrontendType.ADMIN) +""" + +import logging +from copy import deepcopy +from dataclasses import dataclass, field + +from sqlalchemy.orm import Session + +from app.modules.base import MenuItemDefinition, MenuSectionDefinition +from app.modules.enums import FrontendType +from app.modules.service import module_service + +logger = logging.getLogger(__name__) + + +@dataclass +class DiscoveredMenuItem: + """ + A menu item discovered from a module, enriched with runtime info. + + Extends MenuItemDefinition with runtime context like visibility status, + module enablement, and resolved route. + """ + + id: str + label_key: str + icon: str + route: str + order: int + is_mandatory: bool + requires_permission: str | None + badge_source: str | None + is_super_admin_only: bool + + # Runtime enrichment + module_code: str + section_id: str + section_label_key: str | None + section_order: int + is_visible: bool = True + is_module_enabled: bool = True + + +@dataclass +class DiscoveredMenuSection: + """ + A menu section discovered from modules, with aggregated items. + + Multiple modules may contribute items to the same section. + """ + + id: str + label_key: str | None + icon: str | None + order: int + is_super_admin_only: bool + is_collapsible: bool + items: list[DiscoveredMenuItem] = field(default_factory=list) + + +class MenuDiscoveryService: + """ + Service to discover and aggregate menu items from all enabled modules. + + This service: + 1. Collects menu definitions from all module definition.py files + 2. Filters by module enablement for the platform + 3. Applies user/platform visibility preferences + 4. Supports permission-based filtering + 5. Returns sorted, renderable menu structures + """ + + def discover_all_menus(self) -> dict[FrontendType, list[MenuSectionDefinition]]: + """ + Discover all menu definitions from all loaded modules. + + Returns: + Dict mapping FrontendType to list of MenuSectionDefinition + from all modules (not filtered by enablement). + """ + from app.modules.registry import MODULES + + all_menus: dict[FrontendType, list[MenuSectionDefinition]] = { + ft: [] for ft in FrontendType + } + + for module_code, module_def in MODULES.items(): + for frontend_type, sections in module_def.menus.items(): + all_menus[frontend_type].extend(deepcopy(sections)) + + return all_menus + + def get_menu_sections_for_frontend( + self, + db: Session, + frontend_type: FrontendType, + platform_id: int | None = None, + ) -> list[DiscoveredMenuSection]: + """ + Get aggregated menu sections for a frontend type. + + Filters by module enablement if platform_id is provided. + Does NOT apply user visibility preferences (use get_menu_for_frontend for that). + + Args: + db: Database session + frontend_type: Frontend type to get menus for + platform_id: Platform ID for module enablement filtering + + Returns: + List of DiscoveredMenuSection sorted by order + """ + from app.modules.registry import MODULES + + # Track sections by ID for aggregation + sections_map: dict[str, DiscoveredMenuSection] = {} + + for module_code, module_def in MODULES.items(): + # Check if module is enabled for this platform + is_module_enabled = True + if platform_id: + is_module_enabled = module_service.is_module_enabled( + db, platform_id, module_code + ) + + # Get menu sections for this frontend type + module_sections = module_def.menus.get(frontend_type, []) + + for section in module_sections: + # Get or create section entry + if section.id not in sections_map: + sections_map[section.id] = DiscoveredMenuSection( + id=section.id, + label_key=section.label_key, + icon=section.icon, + order=section.order, + is_super_admin_only=section.is_super_admin_only, + is_collapsible=section.is_collapsible, + items=[], + ) + + # Add items from this module to the section + for item in section.items: + discovered_item = DiscoveredMenuItem( + id=item.id, + label_key=item.label_key, + icon=item.icon, + route=item.route, + order=item.order, + is_mandatory=item.is_mandatory, + requires_permission=item.requires_permission, + badge_source=item.badge_source, + is_super_admin_only=item.is_super_admin_only, + module_code=module_code, + section_id=section.id, + section_label_key=section.label_key, + section_order=section.order, + is_module_enabled=is_module_enabled, + ) + sections_map[section.id].items.append(discovered_item) + + # Sort sections by order + sorted_sections = sorted(sections_map.values(), key=lambda s: s.order) + + # Sort items within each section + for section in sorted_sections: + section.items.sort(key=lambda i: i.order) + + return sorted_sections + + def get_menu_for_frontend( + self, + db: Session, + frontend_type: FrontendType, + platform_id: int | None = None, + user_id: int | None = None, + is_super_admin: bool = False, + vendor_code: str | None = None, + ) -> list[DiscoveredMenuSection]: + """ + Get filtered menu structure for frontend rendering. + + Applies all filters: + 1. Module enablement (disabled modules = hidden items) + 2. Visibility configuration (AdminMenuConfig preferences) + 3. Super admin status (hides super_admin_only items for non-super-admins) + 4. Permission requirements (future: filter by user permissions) + + Args: + db: Database session + frontend_type: Frontend type (ADMIN, VENDOR, etc.) + platform_id: Platform ID for module enablement and visibility + user_id: User ID for user-specific visibility (super admins only) + is_super_admin: Whether the user is a super admin + vendor_code: Vendor code for route placeholder replacement + + Returns: + List of DiscoveredMenuSection with filtered and sorted items + """ + # Get all sections with module enablement filtering + sections = self.get_menu_sections_for_frontend(db, frontend_type, platform_id) + + # Get visibility configuration + visible_item_ids = self._get_visible_item_ids( + db, frontend_type, platform_id, user_id + ) + + # Filter sections and items + filtered_sections = [] + for section in sections: + # Skip super_admin_only sections for non-super-admins + if section.is_super_admin_only and not is_super_admin: + continue + + # Filter items + filtered_items = [] + for item in section.items: + # Skip if module is disabled + if not item.is_module_enabled: + continue + + # Skip super_admin_only items for non-super-admins + if item.is_super_admin_only and not is_super_admin: + continue + + # Apply visibility (mandatory items always visible) + if visible_item_ids is not None and not item.is_mandatory: + if item.id not in visible_item_ids: + continue + + # Resolve route placeholders + if vendor_code and "{vendor_code}" in item.route: + item.route = item.route.replace("{vendor_code}", vendor_code) + + item.is_visible = True + filtered_items.append(item) + + # Only include section if it has visible items + if filtered_items: + section.items = filtered_items + filtered_sections.append(section) + + return filtered_sections + + def _get_visible_item_ids( + self, + db: Session, + frontend_type: FrontendType, + platform_id: int | None = None, + user_id: int | None = None, + ) -> set[str] | None: + """ + Get set of visible menu item IDs from AdminMenuConfig. + + Returns: + Set of visible item IDs, or None if no config exists (default all visible) + """ + from app.modules.core.models import AdminMenuConfig + + if not platform_id and not user_id: + return None + + query = db.query(AdminMenuConfig).filter( + AdminMenuConfig.frontend_type == frontend_type, + ) + + if platform_id: + query = query.filter(AdminMenuConfig.platform_id == platform_id) + elif user_id: + query = query.filter(AdminMenuConfig.user_id == user_id) + + configs = query.all() + if not configs: + return None # No config = all visible by default + + return {c.menu_item_id for c in configs if c.is_visible} + + def get_all_menu_items( + self, + frontend_type: FrontendType, + ) -> list[DiscoveredMenuItem]: + """ + Get flat list of all menu items for a frontend type. + + Useful for configuration UI where you need to show all possible items. + Does NOT filter by module enablement or visibility. + + Args: + frontend_type: Frontend type to get items for + + Returns: + Flat list of DiscoveredMenuItem from all modules + """ + from app.modules.registry import MODULES + + items = [] + + for module_code, module_def in MODULES.items(): + for section in module_def.menus.get(frontend_type, []): + for item in section.items: + discovered_item = DiscoveredMenuItem( + id=item.id, + label_key=item.label_key, + icon=item.icon, + route=item.route, + order=item.order, + is_mandatory=item.is_mandatory, + requires_permission=item.requires_permission, + badge_source=item.badge_source, + is_super_admin_only=item.is_super_admin_only, + module_code=module_code, + section_id=section.id, + section_label_key=section.label_key, + section_order=section.order, + ) + items.append(discovered_item) + + return sorted(items, key=lambda i: (i.section_order, i.order)) + + def get_mandatory_item_ids( + self, + frontend_type: FrontendType, + ) -> set[str]: + """ + Get all mandatory menu item IDs for a frontend type. + + Mandatory items cannot be hidden by users. + + Args: + frontend_type: Frontend type to get mandatory items for + + Returns: + Set of menu item IDs marked as is_mandatory=True + """ + from app.modules.registry import MODULES + + mandatory_ids = set() + + for module_def in MODULES.values(): + for section in module_def.menus.get(frontend_type, []): + for item in section.items: + if item.is_mandatory: + mandatory_ids.add(item.id) + + return mandatory_ids + + def get_menu_item_module( + self, + menu_item_id: str, + frontend_type: FrontendType, + ) -> str | None: + """ + Get the module code that provides a specific menu item. + + Args: + menu_item_id: Menu item ID to look up + frontend_type: Frontend type to search in + + Returns: + Module code, or None if not found + """ + from app.modules.registry import MODULES + + for module_code, module_def in MODULES.items(): + for section in module_def.menus.get(frontend_type, []): + for item in section.items: + if item.id == menu_item_id: + return module_code + + return None + + def menu_to_legacy_format( + self, + sections: list[DiscoveredMenuSection], + ) -> dict: + """ + Convert discovered menu sections to legacy registry format. + + This allows gradual migration by using new discovery with old rendering. + + Args: + sections: List of DiscoveredMenuSection + + Returns: + Dict in ADMIN_MENU_REGISTRY/VENDOR_MENU_REGISTRY format + """ + return { + "sections": [ + { + "id": section.id, + "label": section.label_key, # Note: key not resolved + "super_admin_only": section.is_super_admin_only, + "items": [ + { + "id": item.id, + "label": item.label_key, # Note: key not resolved + "icon": item.icon, + "url": item.route, + "super_admin_only": item.is_super_admin_only, + } + for item in section.items + ], + } + for section in sections + ] + } + + +# Singleton instance +menu_discovery_service = MenuDiscoveryService() + + +__all__ = [ + "menu_discovery_service", + "MenuDiscoveryService", + "DiscoveredMenuItem", + "DiscoveredMenuSection", +] diff --git a/app/modules/core/services/menu_service.py b/app/modules/core/services/menu_service.py index 98cd68dd..d7bee972 100644 --- a/app/modules/core/services/menu_service.py +++ b/app/modules/core/services/menu_service.py @@ -42,11 +42,9 @@ from app.config.menu_registry import ( is_super_admin_only_item, ) from app.modules.service import module_service -from models.database.admin_menu_config import ( - AdminMenuConfig, - FrontendType, - MANDATORY_MENU_ITEMS, -) +from app.modules.core.models import AdminMenuConfig, MANDATORY_MENU_ITEMS +from app.modules.core.services.menu_discovery_service import menu_discovery_service +from app.modules.enums import FrontendType logger = logging.getLogger(__name__) @@ -236,10 +234,13 @@ class MenuService: platform_id: int | None = None, user_id: int | None = None, is_super_admin: bool = False, + vendor_code: str | None = None, ) -> dict: """ Get filtered menu structure for frontend rendering. + Uses MenuDiscoveryService to aggregate menus from all enabled modules. + Filters by: 1. Module enablement (items from disabled modules are removed) 2. Visibility configuration @@ -251,40 +252,23 @@ class MenuService: platform_id: Platform ID (for platform admins and vendors) user_id: User ID (for super admins only) is_super_admin: Whether user is super admin (affects admin-only sections) + vendor_code: Vendor code for URL placeholder replacement (vendor frontend) Returns: Filtered menu structure ready for rendering """ - registry = ( - ADMIN_MENU_REGISTRY if frontend_type == FrontendType.ADMIN else VENDOR_MENU_REGISTRY + # Use the module-driven discovery service to get filtered menu + sections = menu_discovery_service.get_menu_for_frontend( + db=db, + frontend_type=frontend_type, + platform_id=platform_id, + user_id=user_id, + is_super_admin=is_super_admin, + vendor_code=vendor_code, ) - visible_items = self.get_visible_menu_items(db, frontend_type, platform_id, user_id) - - # Deep copy to avoid modifying the registry - filtered_menu = deepcopy(registry) - filtered_sections = [] - - for section in filtered_menu["sections"]: - # Skip super_admin_only sections if user is not super admin - if section.get("super_admin_only") and not is_super_admin: - continue - - # Filter items to only visible ones - # Also skip super_admin_only items if user is not super admin - filtered_items = [ - item for item in section["items"] - if item["id"] in visible_items - and (not item.get("super_admin_only") or is_super_admin) - ] - - # Only include section if it has visible items - if filtered_items: - section["items"] = filtered_items - filtered_sections.append(section) - - filtered_menu["sections"] = filtered_sections - return filtered_menu + # Convert to legacy format for backwards compatibility with existing templates + return menu_discovery_service.menu_to_legacy_format(sections) # ========================================================================= # Menu Configuration (Super Admin) diff --git a/app/modules/core/services/platform_settings_service.py b/app/modules/core/services/platform_settings_service.py index 275bb550..dc9fec88 100644 --- a/app/modules/core/services/platform_settings_service.py +++ b/app/modules/core/services/platform_settings_service.py @@ -17,7 +17,7 @@ from typing import Any from sqlalchemy.orm import Session from app.core.config import settings -from models.database.admin import AdminSetting +from app.modules.tenancy.models import AdminSetting logger = logging.getLogger(__name__) diff --git a/app/modules/core/static/admin/js/dashboard.js b/app/modules/core/static/admin/js/dashboard.js index e0c5a79e..c225a578 100644 --- a/app/modules/core/static/admin/js/dashboard.js +++ b/app/modules/core/static/admin/js/dashboard.js @@ -25,6 +25,9 @@ function adminDashboard() { * Initialize dashboard */ async init() { + // Load i18n translations + await I18n.loadModule('core'); + // Guard against multiple initialization if (window._dashboardInitialized) { dashLog.warn('Dashboard already initialized, skipping...'); @@ -79,7 +82,7 @@ function adminDashboard() { } catch (error) { window.LogConfig.logError(error, 'Dashboard Load'); this.error = error.message; - Utils.showToast('Failed to load dashboard data', 'error'); + Utils.showToast(I18n.t('core.messages.failed_to_load_dashboard_data'), 'error'); } finally { this.loading = false; @@ -182,7 +185,7 @@ function adminDashboard() { async refresh() { dashLog.info('=== DASHBOARD REFRESH TRIGGERED ==='); await this.loadDashboard(); - Utils.showToast('Dashboard refreshed', 'success'); + Utils.showToast(I18n.t('core.messages.dashboard_refreshed'), 'success'); dashLog.info('=== DASHBOARD REFRESH COMPLETE ==='); } }; diff --git a/app/modules/core/utils/page_context.py b/app/modules/core/utils/page_context.py index 90c6bcd8..cb6f07fd 100644 --- a/app/modules/core/utils/page_context.py +++ b/app/modules/core/utils/page_context.py @@ -14,8 +14,8 @@ from sqlalchemy.orm import Session from app.core.config import settings from app.modules.core.services.platform_settings_service import platform_settings_service from app.utils.i18n import get_jinja2_globals -from models.database.user import User -from models.database.vendor import Vendor +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/customers/definition.py b/app/modules/customers/definition.py index 95fcf711..6cafaef3 100644 --- a/app/modules/customers/definition.py +++ b/app/modules/customers/definition.py @@ -6,8 +6,8 @@ Defines the customers module including its features, menu items, route configurations, and self-contained module settings. """ -from app.modules.base import ModuleDefinition -from models.database.admin_menu_config import FrontendType +from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition +from app.modules.enums import FrontendType def _get_admin_router(): @@ -46,6 +46,43 @@ customers_module = ModuleDefinition( "customers", # Vendor customer list ], }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="vendorOps", + label_key="customers.menu.vendor_operations", + icon="user-group", + order=40, + items=[ + MenuItemDefinition( + id="customers", + label_key="customers.menu.customers", + icon="user-group", + route="/admin/customers", + order=20, + ), + ], + ), + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="customers", + label_key="customers.menu.customers_section", + icon="user-group", + order=30, + items=[ + MenuItemDefinition( + id="customers", + label_key="customers.menu.all_customers", + icon="user-group", + route="/vendor/{vendor_code}/customers", + order=10, + ), + ], + ), + ], + }, is_core=True, # Customers is a core module - customer data is fundamental # ========================================================================= # Self-Contained Module Configuration diff --git a/app/modules/customers/locales/de.json b/app/modules/customers/locales/de.json index 0967ef42..2967712d 100644 --- a/app/modules/customers/locales/de.json +++ b/app/modules/customers/locales/de.json @@ -1 +1,21 @@ -{} +{ + "customers": { + "title": "Kunden", + "customer": "Kunde", + "add_customer": "Kunde hinzufügen", + "edit_customer": "Kunde bearbeiten", + "customer_name": "Kundenname", + "customer_email": "Kunden-E-Mail", + "customer_phone": "Kundentelefon", + "customer_number": "Kundennummer", + "first_name": "Vorname", + "last_name": "Nachname", + "company": "Firma", + "total_orders": "Bestellungen gesamt", + "total_spent": "Gesamtausgaben", + "last_order": "Letzte Bestellung", + "registered": "Registriert", + "no_customers": "Keine Kunden gefunden", + "search_customers": "Kunden suchen..." + } +} diff --git a/app/modules/customers/locales/en.json b/app/modules/customers/locales/en.json index 0967ef42..a7a6c835 100644 --- a/app/modules/customers/locales/en.json +++ b/app/modules/customers/locales/en.json @@ -1 +1,26 @@ -{} +{ + "customers": { + "title": "Customers", + "customer": "Customer", + "add_customer": "Add Customer", + "edit_customer": "Edit Customer", + "customer_name": "Customer Name", + "customer_email": "Customer Email", + "customer_phone": "Customer Phone", + "customer_number": "Customer Number", + "first_name": "First Name", + "last_name": "Last Name", + "company": "Company", + "total_orders": "Total Orders", + "total_spent": "Total Spent", + "last_order": "Last Order", + "registered": "Registered", + "no_customers": "No customers found", + "search_customers": "Search customers..." + }, + "messages": { + "failed_to_toggle_customer_status": "Failed to toggle customer status", + "failed_to_load_customer_details": "Failed to load customer details", + "failed_to_load_customer_orders": "Failed to load customer orders" + } +} diff --git a/app/modules/customers/locales/fr.json b/app/modules/customers/locales/fr.json index 0967ef42..9268b5b4 100644 --- a/app/modules/customers/locales/fr.json +++ b/app/modules/customers/locales/fr.json @@ -1 +1,21 @@ -{} +{ + "customers": { + "title": "Clients", + "customer": "Client", + "add_customer": "Ajouter un client", + "edit_customer": "Modifier le client", + "customer_name": "Nom du client", + "customer_email": "E-mail du client", + "customer_phone": "Téléphone du client", + "customer_number": "Numéro client", + "first_name": "Prénom", + "last_name": "Nom", + "company": "Entreprise", + "total_orders": "Total des commandes", + "total_spent": "Total dépensé", + "last_order": "Dernière commande", + "registered": "Inscrit", + "no_customers": "Aucun client trouvé", + "search_customers": "Rechercher des clients..." + } +} diff --git a/app/modules/customers/locales/lb.json b/app/modules/customers/locales/lb.json index 0967ef42..e3a6e1b3 100644 --- a/app/modules/customers/locales/lb.json +++ b/app/modules/customers/locales/lb.json @@ -1 +1,21 @@ -{} +{ + "customers": { + "title": "Clienten", + "customer": "Client", + "add_customer": "Client derbäisetzen", + "edit_customer": "Client änneren", + "customer_name": "Clientennumm", + "customer_email": "Client E-Mail", + "customer_phone": "Client Telefon", + "customer_number": "Clientennummer", + "first_name": "Virnumm", + "last_name": "Nonumm", + "company": "Firma", + "total_orders": "Bestellungen insgesamt", + "total_spent": "Total ausginn", + "last_order": "Lescht Bestellung", + "registered": "Registréiert", + "no_customers": "Keng Clienten fonnt", + "search_customers": "Clienten sichen..." + } +} diff --git a/app/modules/customers/routes/pages/admin.py b/app/modules/customers/routes/pages/admin.py index 5f33b8fc..dde39ba2 100644 --- a/app/modules/customers/routes/pages/admin.py +++ b/app/modules/customers/routes/pages/admin.py @@ -13,8 +13,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context from app.templates_config import templates -from models.database.admin_menu_config import FrontendType -from models.database.user import User +from app.modules.enums import FrontendType +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/customers/routes/pages/vendor.py b/app/modules/customers/routes/pages/vendor.py index 22190b5b..77a276a3 100644 --- a/app/modules/customers/routes/pages/vendor.py +++ b/app/modules/customers/routes/pages/vendor.py @@ -13,7 +13,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_vendor_context from app.templates_config import templates -from models.database.user import User +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/customers/services/admin_customer_service.py b/app/modules/customers/services/admin_customer_service.py index a6191817..d07944cf 100644 --- a/app/modules/customers/services/admin_customer_service.py +++ b/app/modules/customers/services/admin_customer_service.py @@ -13,7 +13,7 @@ from sqlalchemy.orm import Session from app.modules.customers.exceptions import CustomerNotFoundException from app.modules.customers.models import Customer -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/customers/services/customer_service.py b/app/modules/customers/services/customer_service.py index 6d73331d..dbc0d923 100644 --- a/app/modules/customers/services/customer_service.py +++ b/app/modules/customers/services/customer_service.py @@ -26,7 +26,7 @@ from app.modules.tenancy.exceptions import VendorNotActiveException, VendorNotFo from app.modules.core.services.auth_service import AuthService from app.modules.customers.models import Customer, PasswordResetToken from app.modules.customers.schemas import CustomerRegister, CustomerUpdate -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/customers/static/admin/js/customers.js b/app/modules/customers/static/admin/js/customers.js index 15a56429..b040cd99 100644 --- a/app/modules/customers/static/admin/js/customers.js +++ b/app/modules/customers/static/admin/js/customers.js @@ -101,6 +101,9 @@ function adminCustomers() { }, async init() { + // Load i18n translations + await I18n.loadModule('customers'); + customersLog.debug('Customers page initialized'); // Load platform settings for rows per page @@ -369,7 +372,7 @@ function adminCustomers() { customersLog.info(response.message); } catch (error) { customersLog.error('Failed to toggle status:', error); - Utils.showToast(error.message || 'Failed to toggle customer status', 'error'); + Utils.showToast(error.message || I18n.t('customers.messages.failed_to_toggle_customer_status'), 'error'); } }, diff --git a/app/modules/customers/static/vendor/js/customers.js b/app/modules/customers/static/vendor/js/customers.js index cf7a8e55..3f232f9d 100644 --- a/app/modules/customers/static/vendor/js/customers.js +++ b/app/modules/customers/static/vendor/js/customers.js @@ -97,6 +97,9 @@ function vendorCustomers() { }, async init() { + // Load i18n translations + await I18n.loadModule('customers'); + vendorCustomersLog.info('Customers init() called'); // Guard against multiple initialization @@ -218,7 +221,7 @@ function vendorCustomers() { vendorCustomersLog.info('Loaded customer details:', customer.id); } catch (error) { vendorCustomersLog.error('Failed to load customer details:', error); - Utils.showToast(error.message || 'Failed to load customer details', 'error'); + Utils.showToast(error.message || I18n.t('customers.messages.failed_to_load_customer_details'), 'error'); } finally { this.loading = false; } @@ -237,7 +240,7 @@ function vendorCustomers() { vendorCustomersLog.info('Loaded customer orders:', customer.id, this.customerOrders.length); } catch (error) { vendorCustomersLog.error('Failed to load customer orders:', error); - Utils.showToast(error.message || 'Failed to load customer orders', 'error'); + Utils.showToast(error.message || I18n.t('customers.messages.failed_to_load_customer_orders'), 'error'); } finally { this.loading = false; } diff --git a/app/modules/dev_tools/definition.py b/app/modules/dev_tools/definition.py index dab35690..08de8680 100644 --- a/app/modules/dev_tools/definition.py +++ b/app/modules/dev_tools/definition.py @@ -12,8 +12,8 @@ Dev-Tools is an internal module providing: - Icon browser """ -from app.modules.base import ModuleDefinition -from models.database.admin_menu_config import FrontendType +from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition +from app.modules.enums import FrontendType # Dev-Tools module definition @@ -46,6 +46,34 @@ dev_tools_module = ModuleDefinition( ], FrontendType.VENDOR: [], # No vendor menu items - internal module }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="devTools", + label_key="dev_tools.menu.developer_tools", + icon="view-grid", + order=85, + is_super_admin_only=True, + items=[ + MenuItemDefinition( + id="components", + label_key="dev_tools.menu.components", + icon="view-grid", + route="/admin/components", + order=10, + ), + MenuItemDefinition( + id="icons", + label_key="dev_tools.menu.icons", + icon="photograph", + route="/admin/icons", + order=20, + ), + ], + ), + ], + }, is_core=False, is_internal=True, # Internal module - admin-only, not customer-facing # ========================================================================= diff --git a/app/modules/dev_tools/locales/de.json b/app/modules/dev_tools/locales/de.json index 0967ef42..9253d042 100644 --- a/app/modules/dev_tools/locales/de.json +++ b/app/modules/dev_tools/locales/de.json @@ -1 +1,12 @@ -{} +{ + "messages": { + "test_run_started": "Test run started...", + "failed_to_copy_code": "Failed to copy code", + "failed_to_copy_name": "Failed to copy name", + "added_to_cart": "Added to cart!", + "filters_applied": "Filters applied!", + "review_submitted_successfully": "Review submitted successfully!", + "thanks_for_your_feedback": "Thanks for your feedback!", + "code_copied_to_clipboard": "Code copied to clipboard!" + } +} diff --git a/app/modules/dev_tools/locales/en.json b/app/modules/dev_tools/locales/en.json index 0967ef42..9253d042 100644 --- a/app/modules/dev_tools/locales/en.json +++ b/app/modules/dev_tools/locales/en.json @@ -1 +1,12 @@ -{} +{ + "messages": { + "test_run_started": "Test run started...", + "failed_to_copy_code": "Failed to copy code", + "failed_to_copy_name": "Failed to copy name", + "added_to_cart": "Added to cart!", + "filters_applied": "Filters applied!", + "review_submitted_successfully": "Review submitted successfully!", + "thanks_for_your_feedback": "Thanks for your feedback!", + "code_copied_to_clipboard": "Code copied to clipboard!" + } +} diff --git a/app/modules/dev_tools/locales/fr.json b/app/modules/dev_tools/locales/fr.json index 0967ef42..9253d042 100644 --- a/app/modules/dev_tools/locales/fr.json +++ b/app/modules/dev_tools/locales/fr.json @@ -1 +1,12 @@ -{} +{ + "messages": { + "test_run_started": "Test run started...", + "failed_to_copy_code": "Failed to copy code", + "failed_to_copy_name": "Failed to copy name", + "added_to_cart": "Added to cart!", + "filters_applied": "Filters applied!", + "review_submitted_successfully": "Review submitted successfully!", + "thanks_for_your_feedback": "Thanks for your feedback!", + "code_copied_to_clipboard": "Code copied to clipboard!" + } +} diff --git a/app/modules/dev_tools/locales/lb.json b/app/modules/dev_tools/locales/lb.json index 0967ef42..9253d042 100644 --- a/app/modules/dev_tools/locales/lb.json +++ b/app/modules/dev_tools/locales/lb.json @@ -1 +1,12 @@ -{} +{ + "messages": { + "test_run_started": "Test run started...", + "failed_to_copy_code": "Failed to copy code", + "failed_to_copy_name": "Failed to copy name", + "added_to_cart": "Added to cart!", + "filters_applied": "Filters applied!", + "review_submitted_successfully": "Review submitted successfully!", + "thanks_for_your_feedback": "Thanks for your feedback!", + "code_copied_to_clipboard": "Code copied to clipboard!" + } +} diff --git a/app/modules/dev_tools/routes/pages/admin.py b/app/modules/dev_tools/routes/pages/admin.py index 222aa032..03cdeee6 100644 --- a/app/modules/dev_tools/routes/pages/admin.py +++ b/app/modules/dev_tools/routes/pages/admin.py @@ -18,8 +18,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context from app.templates_config import templates -from models.database.admin_menu_config import FrontendType -from models.database.user import User +from app.modules.enums import FrontendType +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/dev_tools/static/admin/js/components.js b/app/modules/dev_tools/static/admin/js/components.js index 8290b46d..8700da5d 100644 --- a/app/modules/dev_tools/static/admin/js/components.js +++ b/app/modules/dev_tools/static/admin/js/components.js @@ -132,7 +132,7 @@ function adminComponents() { this.addedToCart = true; setTimeout(() => { this.addedToCart = false; }, 2000); if (typeof Utils !== 'undefined' && Utils.showToast) { - Utils.showToast('Added to cart!', 'success'); + Utils.showToast(I18n.t('dev_tools.messages.added_to_cart'), 'success'); } }, 800); }, @@ -320,7 +320,7 @@ function adminComponents() { demoFilterProducts() { componentsLog.debug('Filtering products with:', this.demoActiveFilters); if (typeof Utils !== 'undefined' && Utils.showToast) { - Utils.showToast('Filters applied!', 'info'); + Utils.showToast(I18n.t('dev_tools.messages.filters_applied'), 'info'); } }, demoSortProducts() { @@ -391,7 +391,7 @@ function adminComponents() { this.showReviewForm = false; this.demoNewReview = { rating: 0, title: '', content: '', images: [] }; if (typeof Utils !== 'undefined' && Utils.showToast) { - Utils.showToast('Review submitted successfully!', 'success'); + Utils.showToast(I18n.t('dev_tools.messages.review_submitted_successfully'), 'success'); } }, 1500); }, @@ -401,7 +401,7 @@ function adminComponents() { review.helpful_count++; } if (typeof Utils !== 'undefined' && Utils.showToast) { - Utils.showToast('Thanks for your feedback!', 'info'); + Utils.showToast(I18n.t('dev_tools.messages.thanks_for_your_feedback'), 'info'); } }, @@ -468,6 +468,9 @@ function adminComponents() { // ✅ CRITICAL: Proper initialization with guard async init() { + // Load i18n translations + await I18n.loadModule('dev_tools'); + componentsLog.info('=== COMPONENTS PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -534,7 +537,7 @@ function adminComponents() { await navigator.clipboard.writeText(code); // Use the global Utils.showToast function if (typeof Utils !== 'undefined' && Utils.showToast) { - Utils.showToast('Code copied to clipboard!', 'success'); + Utils.showToast(I18n.t('dev_tools.messages.code_copied_to_clipboard'), 'success'); } else { componentsLog.warn('Utils.showToast not available'); } @@ -542,7 +545,7 @@ function adminComponents() { } catch (error) { window.LogConfig.logError(error, 'Copy Code'); if (typeof Utils !== 'undefined' && Utils.showToast) { - Utils.showToast('Failed to copy code', 'error'); + Utils.showToast(I18n.t('dev_tools.messages.failed_to_copy_code'), 'error'); } } }, diff --git a/app/modules/dev_tools/static/admin/js/icons-page.js b/app/modules/dev_tools/static/admin/js/icons-page.js index cbb1ea0e..b6fcd87f 100644 --- a/app/modules/dev_tools/static/admin/js/icons-page.js +++ b/app/modules/dev_tools/static/admin/js/icons-page.js @@ -45,6 +45,9 @@ function adminIcons() { // ✅ CRITICAL: Proper initialization with guard async init() { + // Load i18n translations + await I18n.loadModule('dev_tools'); + iconsLog.info('=== ICONS PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -169,7 +172,7 @@ function adminIcons() { iconsLog.debug('Icon usage code copied:', iconName); } catch (error) { window.LogConfig.logError(error, 'Copy Icon Usage'); - Utils.showToast('Failed to copy code', 'error'); + Utils.showToast(I18n.t('dev_tools.messages.failed_to_copy_code'), 'error'); } }, @@ -183,7 +186,7 @@ function adminIcons() { iconsLog.debug('Icon name copied:', iconName); } catch (error) { window.LogConfig.logError(error, 'Copy Icon Name'); - Utils.showToast('Failed to copy name', 'error'); + Utils.showToast(I18n.t('dev_tools.messages.failed_to_copy_name'), 'error'); } }, diff --git a/app/modules/dev_tools/static/admin/js/testing-dashboard.js b/app/modules/dev_tools/static/admin/js/testing-dashboard.js index 183f7881..a9e5d8c7 100644 --- a/app/modules/dev_tools/static/admin/js/testing-dashboard.js +++ b/app/modules/dev_tools/static/admin/js/testing-dashboard.js @@ -54,6 +54,9 @@ function testingDashboard() { runs: [], async init() { + // Load i18n translations + await I18n.loadModule('dev_tools'); + // Guard against multiple initialization if (window._adminTestingDashboardInitialized) return; window._adminTestingDashboardInitialized = true; @@ -148,7 +151,7 @@ function testingDashboard() { // Start polling for status this.pollInterval = setInterval(() => this.pollRunStatus(), 2000); - Utils.showToast('Test run started...', 'info'); + Utils.showToast(I18n.t('dev_tools.messages.test_run_started'), 'info'); } catch (err) { testingDashboardLog.error('Failed to start tests:', err); diff --git a/app/modules/enums.py b/app/modules/enums.py index 13a950ff..d6c9fc1c 100644 --- a/app/modules/enums.py +++ b/app/modules/enums.py @@ -21,12 +21,15 @@ import enum class FrontendType(str, enum.Enum): """Frontend types that can have menu configuration.""" - ADMIN = "admin" # Admin panel (super admins, platform admins) - VENDOR = "vendor" # Vendor dashboard + PLATFORM = "platform" # Public marketing pages (pricing, signup, about) + ADMIN = "admin" # Admin panel (super admins, platform admins) + VENDOR = "vendor" # Vendor dashboard + STOREFRONT = "storefront" # Customer-facing shop # Menu items that cannot be hidden - always visible regardless of config # Organized by frontend type +# NOTE: This will be deprecated in favor of MenuItemDefinition.is_mandatory MANDATORY_MENU_ITEMS = { FrontendType.ADMIN: frozenset({ "dashboard", # Default landing page after login @@ -40,6 +43,8 @@ MANDATORY_MENU_ITEMS = { "dashboard", # Default landing page after login "settings", }), + FrontendType.PLATFORM: frozenset(), + FrontendType.STOREFRONT: frozenset(), } diff --git a/app/modules/inventory/definition.py b/app/modules/inventory/definition.py index 56bbf4e2..4b64d1ff 100644 --- a/app/modules/inventory/definition.py +++ b/app/modules/inventory/definition.py @@ -6,8 +6,8 @@ Defines the inventory module including its features, menu items, route configurations, and self-contained module settings. """ -from app.modules.base import ModuleDefinition -from models.database.admin_menu_config import FrontendType +from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition +from app.modules.enums import FrontendType def _get_admin_router(): @@ -52,6 +52,50 @@ inventory_module = ModuleDefinition( "inventory", # Vendor inventory management ], }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="vendorOps", + label_key="inventory.menu.vendor_operations", + icon="cube", + order=40, + items=[ + MenuItemDefinition( + id="vendor-products", + label_key="inventory.menu.products", + icon="cube", + route="/admin/vendor-products", + order=10, + ), + MenuItemDefinition( + id="inventory", + label_key="inventory.menu.inventory", + icon="archive", + route="/admin/inventory", + order=30, + ), + ], + ), + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="products", + label_key="inventory.menu.products_inventory", + icon="package", + order=10, + items=[ + MenuItemDefinition( + id="inventory", + label_key="inventory.menu.inventory", + icon="clipboard-list", + route="/vendor/{vendor_code}/inventory", + order=20, + ), + ], + ), + ], + }, is_core=False, # ========================================================================= # Self-Contained Module Configuration diff --git a/app/modules/inventory/locales/de.json b/app/modules/inventory/locales/de.json index 0967ef42..66b21a90 100644 --- a/app/modules/inventory/locales/de.json +++ b/app/modules/inventory/locales/de.json @@ -1 +1,22 @@ -{} +{ + "inventory": { + "title": "Inventar", + "stock_level": "Lagerbestand", + "quantity": "Menge", + "reorder_point": "Nachbestellpunkt", + "adjust_stock": "Bestand anpassen", + "stock_in": "Wareneingang", + "stock_out": "Warenausgang", + "transfer": "Transfer", + "history": "Verlauf", + "low_stock_alert": "Warnung bei geringem Bestand", + "out_of_stock_alert": "Warnung bei Ausverkauf" + }, + "messages": { + "stock_adjusted_successfully": "Stock adjusted successfully", + "quantity_set_successfully": "Quantity set successfully", + "inventory_entry_deleted": "Inventory entry deleted.", + "please_select_a_vendor_and_file": "Please select a vendor and file", + "import_completed_with_errors": "Import completed with errors" + } +} diff --git a/app/modules/inventory/locales/en.json b/app/modules/inventory/locales/en.json index 0967ef42..d5b8e55f 100644 --- a/app/modules/inventory/locales/en.json +++ b/app/modules/inventory/locales/en.json @@ -1 +1,31 @@ -{} +{ + "inventory": { + "title": "Inventory", + "stock_level": "Stock Level", + "quantity": "Quantity", + "reorder_point": "Reorder Point", + "adjust_stock": "Adjust Stock", + "stock_in": "Stock In", + "stock_out": "Stock Out", + "transfer": "Transfer", + "history": "History", + "low_stock_alert": "Low Stock Alert", + "out_of_stock_alert": "Out of Stock Alert" + }, + "messages": { + "stock_adjusted_successfully": "Stock adjusted successfully", + "quantity_set_successfully": "Quantity set successfully", + "inventory_entry_deleted": "Inventory entry deleted.", + "please_select_a_vendor_and_file": "Please select a vendor and file", + "import_completed_with_errors": "Import completed with errors", + "failed_to_adjust_stock": "Failed to adjust stock.", + "failed_to_set_quantity": "Failed to set quantity.", + "failed_to_delete_entry": "Failed to delete entry.", + "import_success": "Imported {quantity} units ({created} new, {updated} updated)", + "import_completed": "Import completed: {created} created, {updated} updated, {errors} errors", + "import_failed": "Import failed", + "bulk_adjust_success": "{count} item(s) adjusted by {amount}", + "failed_to_adjust_inventory": "Failed to adjust inventory", + "exported_items": "Exported {count} item(s)" + } +} diff --git a/app/modules/inventory/locales/fr.json b/app/modules/inventory/locales/fr.json index 0967ef42..02d666e5 100644 --- a/app/modules/inventory/locales/fr.json +++ b/app/modules/inventory/locales/fr.json @@ -1 +1,22 @@ -{} +{ + "inventory": { + "title": "Inventaire", + "stock_level": "Niveau de stock", + "quantity": "Quantité", + "reorder_point": "Seuil de réapprovisionnement", + "adjust_stock": "Ajuster le stock", + "stock_in": "Entrée de stock", + "stock_out": "Sortie de stock", + "transfer": "Transfert", + "history": "Historique", + "low_stock_alert": "Alerte stock faible", + "out_of_stock_alert": "Alerte rupture de stock" + }, + "messages": { + "stock_adjusted_successfully": "Stock adjusted successfully", + "quantity_set_successfully": "Quantity set successfully", + "inventory_entry_deleted": "Inventory entry deleted.", + "please_select_a_vendor_and_file": "Please select a vendor and file", + "import_completed_with_errors": "Import completed with errors" + } +} diff --git a/app/modules/inventory/locales/lb.json b/app/modules/inventory/locales/lb.json index 0967ef42..2c3b3515 100644 --- a/app/modules/inventory/locales/lb.json +++ b/app/modules/inventory/locales/lb.json @@ -1 +1,22 @@ -{} +{ + "inventory": { + "title": "Inventar", + "stock_level": "Lagerniveau", + "quantity": "Quantitéit", + "reorder_point": "Nobestellungspunkt", + "adjust_stock": "Lager upaassen", + "stock_in": "Lager eran", + "stock_out": "Lager eraus", + "transfer": "Transfer", + "history": "Geschicht", + "low_stock_alert": "Niddreg Lager Alarm", + "out_of_stock_alert": "Net op Lager Alarm" + }, + "messages": { + "stock_adjusted_successfully": "Stock adjusted successfully", + "quantity_set_successfully": "Quantity set successfully", + "inventory_entry_deleted": "Inventory entry deleted.", + "please_select_a_vendor_and_file": "Please select a vendor and file", + "import_completed_with_errors": "Import completed with errors" + } +} diff --git a/app/modules/inventory/routes/pages/admin.py b/app/modules/inventory/routes/pages/admin.py index b4bea7da..b4ecf5eb 100644 --- a/app/modules/inventory/routes/pages/admin.py +++ b/app/modules/inventory/routes/pages/admin.py @@ -13,8 +13,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_db, require_menu_access from app.modules.core.utils.page_context import get_admin_context from app.templates_config import templates -from models.database.admin_menu_config import FrontendType -from models.database.user import User +from app.modules.enums import FrontendType +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/inventory/routes/pages/vendor.py b/app/modules/inventory/routes/pages/vendor.py index 8a4741a3..c1604a19 100644 --- a/app/modules/inventory/routes/pages/vendor.py +++ b/app/modules/inventory/routes/pages/vendor.py @@ -13,7 +13,7 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_vendor_context from app.templates_config import templates -from models.database.user import User +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/inventory/services/inventory_service.py b/app/modules/inventory/services/inventory_service.py index 055982b0..24598c71 100644 --- a/app/modules/inventory/services/inventory_service.py +++ b/app/modules/inventory/services/inventory_service.py @@ -31,7 +31,7 @@ from app.modules.inventory.schemas.inventory import ( ProductInventorySummary, ) from app.modules.catalog.models import Product -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/inventory/services/inventory_transaction_service.py b/app/modules/inventory/services/inventory_transaction_service.py index 7366bad9..f67a727f 100644 --- a/app/modules/inventory/services/inventory_transaction_service.py +++ b/app/modules/inventory/services/inventory_transaction_service.py @@ -319,7 +319,7 @@ class InventoryTransactionService: Returns: Tuple of (transactions with details, total count) """ - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor # Build query query = db.query(InventoryTransaction) diff --git a/app/modules/inventory/static/admin/js/inventory.js b/app/modules/inventory/static/admin/js/inventory.js index 42d940db..e1dcd6de 100644 --- a/app/modules/inventory/static/admin/js/inventory.js +++ b/app/modules/inventory/static/admin/js/inventory.js @@ -138,6 +138,9 @@ function adminInventory() { }, async init() { + // Load i18n translations + await I18n.loadModule('inventory'); + adminInventoryLog.info('Inventory init() called'); // Guard against multiple initialization @@ -413,12 +416,12 @@ function adminInventory() { this.showAdjustModal = false; this.selectedItem = null; - Utils.showToast('Stock adjusted successfully.', 'success'); + Utils.showToast(I18n.t('inventory.messages.stock_adjusted_successfully'), 'success'); await this.refresh(); } catch (error) { adminInventoryLog.error('Failed to adjust inventory:', error); - Utils.showToast(error.message || 'Failed to adjust stock.', 'error'); + Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_adjust_stock'), 'error'); } finally { this.saving = false; } @@ -444,12 +447,12 @@ function adminInventory() { this.showSetModal = false; this.selectedItem = null; - Utils.showToast('Quantity set successfully.', 'success'); + Utils.showToast(I18n.t('inventory.messages.quantity_set_successfully'), 'success'); await this.refresh(); } catch (error) { adminInventoryLog.error('Failed to set inventory:', error); - Utils.showToast(error.message || 'Failed to set quantity.', 'error'); + Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_set_quantity'), 'error'); } finally { this.saving = false; } @@ -470,12 +473,12 @@ function adminInventory() { this.showDeleteModal = false; this.selectedItem = null; - Utils.showToast('Inventory entry deleted.', 'success'); + Utils.showToast(I18n.t('inventory.messages.inventory_entry_deleted'), 'success'); await this.refresh(); } catch (error) { adminInventoryLog.error('Failed to delete inventory:', error); - Utils.showToast(error.message || 'Failed to delete entry.', 'error'); + Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_delete_entry'), 'error'); } finally { this.saving = false; } @@ -540,7 +543,7 @@ function adminInventory() { */ async executeImport() { if (!this.importForm.vendor_id || !this.importForm.file) { - Utils.showToast('Please select a vendor and file', 'error'); + Utils.showToast(I18n.t('inventory.messages.please_select_a_vendor_and_file'), 'error'); return; } @@ -559,13 +562,17 @@ function adminInventory() { if (this.importResult.success) { adminInventoryLog.info('Import successful:', this.importResult); Utils.showToast( - `Imported ${this.importResult.quantity_imported} units (${this.importResult.entries_created} new, ${this.importResult.entries_updated} updated)`, + I18n.t('inventory.messages.import_success', { + quantity: this.importResult.quantity_imported, + created: this.importResult.entries_created, + updated: this.importResult.entries_updated + }), 'success' ); // Refresh inventory list await this.refresh(); } else { - Utils.showToast('Import completed with errors', 'warning'); + Utils.showToast(I18n.t('inventory.messages.import_completed_with_errors'), 'warning'); } } catch (error) { adminInventoryLog.error('Import failed:', error); @@ -573,7 +580,7 @@ function adminInventory() { success: false, errors: [error.message || 'Import failed'] }; - Utils.showToast(error.message || 'Import failed', 'error'); + Utils.showToast(error.message || I18n.t('inventory.messages.import_failed'), 'error'); } finally { this.importing = false; } diff --git a/app/modules/inventory/static/vendor/js/inventory.js b/app/modules/inventory/static/vendor/js/inventory.js index b29f6448..303fc873 100644 --- a/app/modules/inventory/static/vendor/js/inventory.js +++ b/app/modules/inventory/static/vendor/js/inventory.js @@ -128,6 +128,9 @@ function vendorInventory() { }, async init() { + // Load i18n translations + await I18n.loadModule('inventory'); + vendorInventoryLog.info('Inventory init() called'); // Guard against multiple initialization @@ -298,12 +301,12 @@ function vendorInventory() { this.showAdjustModal = false; this.selectedItem = null; - Utils.showToast('Stock adjusted successfully', 'success'); + Utils.showToast(I18n.t('inventory.messages.stock_adjusted_successfully'), 'success'); await this.loadInventory(); } catch (error) { vendorInventoryLog.error('Failed to adjust inventory:', error); - Utils.showToast(error.message || 'Failed to adjust stock', 'error'); + Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_adjust_stock'), 'error'); } finally { this.saving = false; } @@ -328,12 +331,12 @@ function vendorInventory() { this.showSetModal = false; this.selectedItem = null; - Utils.showToast('Quantity set successfully', 'success'); + Utils.showToast(I18n.t('inventory.messages.quantity_set_successfully'), 'success'); await this.loadInventory(); } catch (error) { vendorInventoryLog.error('Failed to set inventory:', error); - Utils.showToast(error.message || 'Failed to set quantity', 'error'); + Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_set_quantity'), 'error'); } finally { this.saving = false; } @@ -465,13 +468,14 @@ function vendorInventory() { } } } - Utils.showToast(`${successCount} item(s) adjusted by ${this.bulkAdjustForm.quantity > 0 ? '+' : ''}${this.bulkAdjustForm.quantity}`, 'success'); + const amount = this.bulkAdjustForm.quantity > 0 ? '+' + this.bulkAdjustForm.quantity : this.bulkAdjustForm.quantity; + Utils.showToast(I18n.t('inventory.messages.bulk_adjust_success', { count: successCount, amount: amount }), 'success'); this.showBulkAdjustModal = false; this.clearSelection(); await this.loadInventory(); } catch (error) { vendorInventoryLog.error('Bulk adjust failed:', error); - Utils.showToast(error.message || 'Failed to adjust inventory', 'error'); + Utils.showToast(error.message || I18n.t('inventory.messages.failed_to_adjust_inventory'), 'error'); } finally { this.saving = false; } @@ -508,7 +512,7 @@ function vendorInventory() { link.download = `inventory_export_${new Date().toISOString().split('T')[0]}.csv`; link.click(); - Utils.showToast(`Exported ${selectedData.length} item(s)`, 'success'); + Utils.showToast(I18n.t('inventory.messages.exported_items', { count: selectedData.length }), 'success'); } }; } diff --git a/app/modules/loyalty/definition.py b/app/modules/loyalty/definition.py index 835d5c97..85f9de2c 100644 --- a/app/modules/loyalty/definition.py +++ b/app/modules/loyalty/definition.py @@ -6,8 +6,8 @@ Defines the loyalty module including its features, menu items, route configurations, and scheduled tasks. """ -from app.modules.base import ModuleDefinition, ScheduledTask -from models.database.admin_menu_config import FrontendType +from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, ScheduledTask +from app.modules.enums import FrontendType def _get_admin_router(): @@ -71,6 +71,64 @@ loyalty_module = ModuleDefinition( "loyalty-stats", # Vendor stats ], }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="loyalty", + label_key="loyalty.menu.loyalty", + icon="gift", + order=55, + items=[ + MenuItemDefinition( + id="loyalty-programs", + label_key="loyalty.menu.programs", + icon="gift", + route="/admin/loyalty/programs", + order=10, + ), + MenuItemDefinition( + id="loyalty-analytics", + label_key="loyalty.menu.analytics", + icon="chart-bar", + route="/admin/loyalty/analytics", + order=20, + ), + ], + ), + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="loyalty", + label_key="loyalty.menu.loyalty_programs", + icon="gift", + order=35, + items=[ + MenuItemDefinition( + id="loyalty", + label_key="loyalty.menu.dashboard", + icon="gift", + route="/vendor/{vendor_code}/loyalty", + order=10, + ), + MenuItemDefinition( + id="loyalty-cards", + label_key="loyalty.menu.customer_cards", + icon="identification", + route="/vendor/{vendor_code}/loyalty/cards", + order=20, + ), + MenuItemDefinition( + id="loyalty-stats", + label_key="loyalty.menu.statistics", + icon="chart-bar", + route="/vendor/{vendor_code}/loyalty/stats", + order=30, + ), + ], + ), + ], + }, is_core=False, # Loyalty can be disabled # ========================================================================= # Self-Contained Module Configuration diff --git a/app/modules/loyalty/routes/api/admin.py b/app/modules/loyalty/routes/api/admin.py index 757a0f80..d4e4ccaa 100644 --- a/app/modules/loyalty/routes/api/admin.py +++ b/app/modules/loyalty/routes/api/admin.py @@ -20,7 +20,7 @@ from app.modules.loyalty.schemas import ( ProgramStatsResponse, ) from app.modules.loyalty.services import program_service -from models.database.user import User +from app.modules.tenancy.models import User logger = logging.getLogger(__name__) diff --git a/app/modules/loyalty/routes/api/public.py b/app/modules/loyalty/routes/api/public.py index 4ca61f0b..a5294aeb 100644 --- a/app/modules/loyalty/routes/api/public.py +++ b/app/modules/loyalty/routes/api/public.py @@ -44,7 +44,7 @@ def get_program_by_vendor_code( db: Session = Depends(get_db), ): """Get loyalty program info by vendor code (for enrollment page).""" - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor # Find vendor by code (vendor_code or subdomain) vendor = ( diff --git a/app/modules/loyalty/routes/api/vendor.py b/app/modules/loyalty/routes/api/vendor.py index 4d32968f..d5561d16 100644 --- a/app/modules/loyalty/routes/api/vendor.py +++ b/app/modules/loyalty/routes/api/vendor.py @@ -53,7 +53,7 @@ from app.modules.loyalty.services import ( stamp_service, wallet_service, ) -from models.database.user import User +from app.modules.tenancy.models import User logger = logging.getLogger(__name__) diff --git a/app/modules/marketplace/definition.py b/app/modules/marketplace/definition.py index 466b03c9..78f7d6ad 100644 --- a/app/modules/marketplace/definition.py +++ b/app/modules/marketplace/definition.py @@ -8,8 +8,8 @@ dependencies, route configurations, and scheduled tasks. Note: This module requires the inventory module to be enabled. """ -from app.modules.base import ModuleDefinition, ScheduledTask -from models.database.admin_menu_config import FrontendType +from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, ScheduledTask +from app.modules.enums import FrontendType def _get_admin_router(): @@ -52,6 +52,58 @@ marketplace_module = ModuleDefinition( "letzshop", # Letzshop integration ], }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="marketplace", + label_key="marketplace.menu.marketplace", + icon="shopping-cart", + order=60, + items=[ + MenuItemDefinition( + id="marketplace-letzshop", + label_key="marketplace.menu.letzshop", + icon="shopping-cart", + route="/admin/marketplace/letzshop", + order=10, + ), + ], + ), + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="products", + label_key="marketplace.menu.products_inventory", + icon="download", + order=10, + items=[ + MenuItemDefinition( + id="marketplace", + label_key="marketplace.menu.marketplace_import", + icon="download", + route="/vendor/{vendor_code}/marketplace", + order=30, + ), + ], + ), + MenuSectionDefinition( + id="sales", + label_key="marketplace.menu.sales_orders", + icon="external-link", + order=20, + items=[ + MenuItemDefinition( + id="letzshop", + label_key="marketplace.menu.letzshop_orders", + icon="external-link", + route="/vendor/{vendor_code}/letzshop", + order=20, + ), + ], + ), + ], + }, is_core=False, # ========================================================================= # Self-Contained Module Configuration diff --git a/app/modules/marketplace/locales/de.json b/app/modules/marketplace/locales/de.json index 62c0816d..9a5e0b3a 100644 --- a/app/modules/marketplace/locales/de.json +++ b/app/modules/marketplace/locales/de.json @@ -1,122 +1,61 @@ { - "title": "Marktplatz-Integration", - "description": "Letzshop Produkt- und Bestellsynchronisation", - "products": { - "title": "Marktplatz-Produkte", - "subtitle": "Von Marktplätzen importierte Produkte", - "empty": "Keine Produkte gefunden", - "empty_search": "Keine Produkte entsprechen Ihrer Suche", - "import": "Produkte importieren" - }, - "import": { - "title": "Produkte importieren", - "subtitle": "Produkte aus Marktplatz-Feeds importieren", - "source_url": "Feed-URL", - "source_url_help": "URL zum Marktplatz-CSV-Feed", - "marketplace": "Marktplatz", - "language": "Sprache", - "language_help": "Sprache für Produktübersetzungen", - "batch_size": "Batch-Größe", + "marketplace": { + "title": "Marktplatz", + "import": "Importieren", + "export": "Exportieren", + "sync": "Synchronisieren", + "source": "Quelle", + "source_url": "Quell-URL", + "import_products": "Produkte importieren", "start_import": "Import starten", - "cancel": "Abbrechen" - }, - "import_jobs": { - "title": "Import-Verlauf", - "subtitle": "Vergangene und aktuelle Import-Jobs", - "empty": "Keine Import-Jobs", - "job_id": "Job-ID", - "marketplace": "Marktplatz", - "vendor": "Verkäufer", - "status": "Status", - "imported": "Importiert", - "updated": "Aktualisiert", - "errors": "Fehler", - "created": "Erstellt", - "completed": "Abgeschlossen", - "statuses": { - "pending": "Ausstehend", - "processing": "In Bearbeitung", - "completed": "Abgeschlossen", - "completed_with_errors": "Mit Fehlern abgeschlossen", - "failed": "Fehlgeschlagen" - } + "importing": "Importiere...", + "import_complete": "Import abgeschlossen", + "import_failed": "Import fehlgeschlagen", + "import_history": "Import-Verlauf", + "job_id": "Auftrags-ID", + "started_at": "Gestartet um", + "completed_at": "Abgeschlossen um", + "duration": "Dauer", + "imported_count": "Importiert", + "error_count": "Fehler", + "total_processed": "Gesamt verarbeitet", + "progress": "Fortschritt", + "no_import_jobs": "Noch keine Imports", + "start_first_import": "Starten Sie Ihren ersten Import mit dem Formular oben" }, "letzshop": { "title": "Letzshop-Integration", - "subtitle": "Letzshop-Verbindung und Synchronisation verwalten", - "credentials": { - "title": "API-Anmeldedaten", - "api_key": "API-Schlüssel", - "api_key_help": "Ihr Letzshop API-Schlüssel", - "endpoint": "API-Endpunkt", - "test_mode": "Testmodus", - "test_mode_help": "Wenn aktiviert, werden keine Änderungen bei Letzshop vorgenommen" - }, - "sync": { - "title": "Synchronisation", - "auto_sync": "Auto-Sync", - "auto_sync_help": "Bestellungen automatisch von Letzshop synchronisieren", - "interval": "Sync-Intervall", - "interval_help": "Minuten zwischen Synchronisationen", - "last_sync": "Letzte Sync", - "last_status": "Letzter Status", - "sync_now": "Jetzt synchronisieren" - }, - "carrier": { - "title": "Versanddiensteinstellungen", - "default_carrier": "Standard-Versanddienstleister", - "greco": "Greco", - "colissimo": "Colissimo", - "xpresslogistics": "XpressLogistics", - "label_url": "Label-URL-Präfix" - }, - "historical": { - "title": "Historischer Import", - "subtitle": "Vergangene Bestellungen von Letzshop importieren", - "start_import": "Historischen Import starten", - "phase": "Phase", - "confirmed": "Bestätigte Bestellungen", - "unconfirmed": "Unbestätigte Bestellungen", - "fetching": "Abrufen...", - "processing": "Verarbeiten...", - "page": "Seite", - "fetched": "Abgerufen", - "processed": "Verarbeitet", - "imported": "Importiert", - "updated": "Aktualisiert", - "skipped": "Übersprungen" - }, - "vendors": { - "title": "Verkäuferverzeichnis", - "subtitle": "Letzshop-Verkäufer durchsuchen", - "claim": "Beanspruchen", - "claimed": "Beansprucht", - "unclaimed": "Nicht beansprucht", - "last_synced": "Zuletzt synchronisiert" + "connection": "Verbindung", + "credentials": "Zugangsdaten", + "api_key": "API-Schlüssel", + "api_endpoint": "API-Endpunkt", + "auto_sync": "Auto-Sync", + "sync_interval": "Sync-Intervall", + "every_hour": "Jede Stunde", + "every_day": "Jeden Tag", + "test_connection": "Verbindung testen", + "save_credentials": "Zugangsdaten speichern", + "connection_success": "Verbindung erfolgreich", + "connection_failed": "Verbindung fehlgeschlagen", + "last_sync": "Letzte Synchronisation", + "sync_status": "Sync-Status", + "import_orders": "Bestellungen importieren", + "export_products": "Produkte exportieren", + "no_credentials": "Konfigurieren Sie Ihren API-Schlüssel in den Einstellungen", + "carriers": { + "dhl": "DHL", + "ups": "UPS", + "fedex": "FedEx", + "dpd": "DPD", + "gls": "GLS", + "post_luxembourg": "Post Luxemburg", + "other": "Andere" } }, - "export": { - "title": "Produkte exportieren", - "subtitle": "Produkte im Marktplatz-Format exportieren", - "format": "Format", - "format_csv": "CSV", - "format_xml": "XML", - "download": "Export herunterladen" - }, "messages": { - "import_started": "Import erfolgreich gestartet", - "import_completed": "Import abgeschlossen", - "import_failed": "Import fehlgeschlagen", - "credentials_saved": "Anmeldedaten erfolgreich gespeichert", - "sync_started": "Synchronisation gestartet", - "sync_completed": "Synchronisation abgeschlossen", - "sync_failed": "Synchronisation fehlgeschlagen", - "export_ready": "Export zum Download bereit", - "error_loading": "Fehler beim Laden der Daten" - }, - "filters": { - "all_marketplaces": "Alle Marktplätze", - "all_vendors": "Alle Verkäufer", - "search_placeholder": "Produkte suchen..." + "no_error_details_available": "No error details available", + "failed_to_load_error_details": "Failed to load error details", + "copied_to_clipboard": "Copied to clipboard", + "failed_to_copy_to_clipboard": "Failed to copy to clipboard" } } diff --git a/app/modules/marketplace/locales/fr.json b/app/modules/marketplace/locales/fr.json index 134cb8c5..9afd6e4f 100644 --- a/app/modules/marketplace/locales/fr.json +++ b/app/modules/marketplace/locales/fr.json @@ -1,122 +1,61 @@ { - "title": "Intégration Marketplace", - "description": "Synchronisation des produits et commandes Letzshop", - "products": { - "title": "Produits Marketplace", - "subtitle": "Produits importés des marketplaces", - "empty": "Aucun produit trouvé", - "empty_search": "Aucun produit ne correspond à votre recherche", - "import": "Importer des produits" - }, - "import": { - "title": "Importer des produits", - "subtitle": "Importer des produits depuis les flux marketplace", - "source_url": "URL du flux", - "source_url_help": "URL du flux CSV marketplace", - "marketplace": "Marketplace", - "language": "Langue", - "language_help": "Langue pour les traductions de produits", - "batch_size": "Taille du lot", - "start_import": "Démarrer l'import", - "cancel": "Annuler" - }, - "import_jobs": { - "title": "Historique des imports", - "subtitle": "Imports passés et en cours", - "empty": "Aucun import", - "job_id": "ID du job", - "marketplace": "Marketplace", - "vendor": "Vendeur", - "status": "Statut", - "imported": "Importés", - "updated": "Mis à jour", - "errors": "Erreurs", - "created": "Créé", - "completed": "Terminé", - "statuses": { - "pending": "En attente", - "processing": "En cours", - "completed": "Terminé", - "completed_with_errors": "Terminé avec erreurs", - "failed": "Échoué" - } + "marketplace": { + "title": "Marketplace", + "import": "Importer", + "export": "Exporter", + "sync": "Synchroniser", + "source": "Source", + "source_url": "URL source", + "import_products": "Importer des produits", + "start_import": "Démarrer l'importation", + "importing": "Importation en cours...", + "import_complete": "Importation terminée", + "import_failed": "Échec de l'importation", + "import_history": "Historique des importations", + "job_id": "ID du travail", + "started_at": "Démarré à", + "completed_at": "Terminé à", + "duration": "Durée", + "imported_count": "Importés", + "error_count": "Erreurs", + "total_processed": "Total traité", + "progress": "Progression", + "no_import_jobs": "Aucune importation pour le moment", + "start_first_import": "Lancez votre première importation avec le formulaire ci-dessus" }, "letzshop": { "title": "Intégration Letzshop", - "subtitle": "Gérer la connexion et la synchronisation Letzshop", - "credentials": { - "title": "Identifiants API", - "api_key": "Clé API", - "api_key_help": "Votre clé API Letzshop", - "endpoint": "Point d'accès API", - "test_mode": "Mode test", - "test_mode_help": "Lorsqu'activé, aucune modification n'est effectuée sur Letzshop" - }, - "sync": { - "title": "Synchronisation", - "auto_sync": "Sync automatique", - "auto_sync_help": "Synchroniser automatiquement les commandes depuis Letzshop", - "interval": "Intervalle de sync", - "interval_help": "Minutes entre les synchronisations", - "last_sync": "Dernière sync", - "last_status": "Dernier statut", - "sync_now": "Synchroniser maintenant" - }, - "carrier": { - "title": "Paramètres transporteur", - "default_carrier": "Transporteur par défaut", - "greco": "Greco", - "colissimo": "Colissimo", - "xpresslogistics": "XpressLogistics", - "label_url": "Préfixe URL étiquette" - }, - "historical": { - "title": "Import historique", - "subtitle": "Importer les commandes passées depuis Letzshop", - "start_import": "Démarrer l'import historique", - "phase": "Phase", - "confirmed": "Commandes confirmées", - "unconfirmed": "Commandes non confirmées", - "fetching": "Récupération...", - "processing": "Traitement...", - "page": "Page", - "fetched": "Récupérées", - "processed": "Traitées", - "imported": "Importées", - "updated": "Mises à jour", - "skipped": "Ignorées" - }, - "vendors": { - "title": "Annuaire des vendeurs", - "subtitle": "Parcourir les vendeurs Letzshop", - "claim": "Revendiquer", - "claimed": "Revendiqué", - "unclaimed": "Non revendiqué", - "last_synced": "Dernière sync" + "connection": "Connexion", + "credentials": "Identifiants", + "api_key": "Clé API", + "api_endpoint": "Point d'accès API", + "auto_sync": "Synchronisation automatique", + "sync_interval": "Intervalle de synchronisation", + "every_hour": "Toutes les heures", + "every_day": "Tous les jours", + "test_connection": "Tester la connexion", + "save_credentials": "Enregistrer les identifiants", + "connection_success": "Connexion réussie", + "connection_failed": "Échec de la connexion", + "last_sync": "Dernière synchronisation", + "sync_status": "Statut de synchronisation", + "import_orders": "Importer les commandes", + "export_products": "Exporter les produits", + "no_credentials": "Configurez votre clé API dans les paramètres pour commencer", + "carriers": { + "dhl": "DHL", + "ups": "UPS", + "fedex": "FedEx", + "dpd": "DPD", + "gls": "GLS", + "post_luxembourg": "Post Luxembourg", + "other": "Autre" } }, - "export": { - "title": "Exporter les produits", - "subtitle": "Exporter les produits au format marketplace", - "format": "Format", - "format_csv": "CSV", - "format_xml": "XML", - "download": "Télécharger l'export" - }, "messages": { - "import_started": "Import démarré avec succès", - "import_completed": "Import terminé", - "import_failed": "Import échoué", - "credentials_saved": "Identifiants enregistrés avec succès", - "sync_started": "Synchronisation démarrée", - "sync_completed": "Synchronisation terminée", - "sync_failed": "Synchronisation échouée", - "export_ready": "Export prêt au téléchargement", - "error_loading": "Erreur lors du chargement des données" - }, - "filters": { - "all_marketplaces": "Tous les marketplaces", - "all_vendors": "Tous les vendeurs", - "search_placeholder": "Rechercher des produits..." + "no_error_details_available": "No error details available", + "failed_to_load_error_details": "Failed to load error details", + "copied_to_clipboard": "Copied to clipboard", + "failed_to_copy_to_clipboard": "Failed to copy to clipboard" } } diff --git a/app/modules/marketplace/locales/lb.json b/app/modules/marketplace/locales/lb.json index 43320db4..5c3d434c 100644 --- a/app/modules/marketplace/locales/lb.json +++ b/app/modules/marketplace/locales/lb.json @@ -1,122 +1,61 @@ { - "title": "Marketplace-Integratioun", - "description": "Letzshop Produkt- a Bestellsynchronisatioun", - "products": { - "title": "Marketplace-Produkter", - "subtitle": "Vun Marketplacen importéiert Produkter", - "empty": "Keng Produkter fonnt", - "empty_search": "Keng Produkter passen zu Ärer Sich", - "import": "Produkter importéieren" - }, - "import": { - "title": "Produkter importéieren", - "subtitle": "Produkter aus Marketplace-Feeds importéieren", - "source_url": "Feed-URL", - "source_url_help": "URL zum Marketplace-CSV-Feed", - "marketplace": "Marketplace", - "language": "Sprooch", - "language_help": "Sprooch fir Produktiwwersetzungen", - "batch_size": "Batch-Gréisst", + "marketplace": { + "title": "Marchéplaz", + "import": "Import", + "export": "Export", + "sync": "Synchroniséieren", + "source": "Quell", + "source_url": "Quell URL", + "import_products": "Produkter importéieren", "start_import": "Import starten", - "cancel": "Ofbriechen" - }, - "import_jobs": { - "title": "Import-Verlaf", - "subtitle": "Vergaangen an aktuell Import-Jobs", - "empty": "Keng Import-Jobs", - "job_id": "Job-ID", - "marketplace": "Marketplace", - "vendor": "Verkeefer", - "status": "Status", - "imported": "Importéiert", - "updated": "Aktualiséiert", - "errors": "Feeler", - "created": "Erstallt", - "completed": "Ofgeschloss", - "statuses": { - "pending": "Waarden", - "processing": "Am Gaang", - "completed": "Ofgeschloss", - "completed_with_errors": "Mat Feeler ofgeschloss", - "failed": "Feelgeschloen" - } + "importing": "Importéieren...", + "import_complete": "Import fäerdeg", + "import_failed": "Import feelgeschloen", + "import_history": "Importgeschicht", + "job_id": "Job ID", + "started_at": "Ugefaang um", + "completed_at": "Fäerdeg um", + "duration": "Dauer", + "imported_count": "Importéiert", + "error_count": "Feeler", + "total_processed": "Total veraarbecht", + "progress": "Fortschrëtt", + "no_import_jobs": "Nach keng Import Jobs", + "start_first_import": "Start Ären éischten Import mat der Form uewendriwwer" }, "letzshop": { - "title": "Letzshop-Integratioun", - "subtitle": "Letzshop-Verbindung a Synchronisatioun verwalten", - "credentials": { - "title": "API-Umeldedaten", - "api_key": "API-Schlëssel", - "api_key_help": "Ären Letzshop API-Schlëssel", - "endpoint": "API-Endpunkt", - "test_mode": "Testmodus", - "test_mode_help": "Wann aktivéiert, ginn keng Ännerungen bei Letzshop gemaach" - }, - "sync": { - "title": "Synchronisatioun", - "auto_sync": "Auto-Sync", - "auto_sync_help": "Bestellungen automatesch vun Letzshop synchroniséieren", - "interval": "Sync-Intervall", - "interval_help": "Minutten tëscht Synchronisatiounen", - "last_sync": "Lescht Sync", - "last_status": "Leschte Status", - "sync_now": "Elo synchroniséieren" - }, - "carrier": { - "title": "Versandastellungen", - "default_carrier": "Standard-Versanddienstleeschter", - "greco": "Greco", - "colissimo": "Colissimo", - "xpresslogistics": "XpressLogistics", - "label_url": "Label-URL-Präfix" - }, - "historical": { - "title": "Historeschen Import", - "subtitle": "Vergaangen Bestellungen vun Letzshop importéieren", - "start_import": "Historeschen Import starten", - "phase": "Phas", - "confirmed": "Bestätegt Bestellungen", - "unconfirmed": "Onbestätegt Bestellungen", - "fetching": "Ofruff...", - "processing": "Veraarbecht...", - "page": "Säit", - "fetched": "Ofgeruff", - "processed": "Veraarbecht", - "imported": "Importéiert", - "updated": "Aktualiséiert", - "skipped": "Iwwersprong" - }, - "vendors": { - "title": "Verkeeferverzeechnes", - "subtitle": "Letzshop-Verkeefer duerchsichen", - "claim": "Reklaméieren", - "claimed": "Reklaméiert", - "unclaimed": "Net reklaméiert", - "last_synced": "Lescht synchroniséiert" + "title": "Letzshop Integratioun", + "connection": "Verbindung", + "credentials": "Umeldungsdaten", + "api_key": "API Schlëssel", + "api_endpoint": "API Endpunkt", + "auto_sync": "Automatesch Sync", + "sync_interval": "Sync Intervall", + "every_hour": "All Stonn", + "every_day": "All Dag", + "test_connection": "Verbindung testen", + "save_credentials": "Umeldungsdaten späicheren", + "connection_success": "Verbindung erfollegräich", + "connection_failed": "Verbindung feelgeschloen", + "last_sync": "Läschte Sync", + "sync_status": "Sync Status", + "import_orders": "Bestellungen importéieren", + "export_products": "Produkter exportéieren", + "no_credentials": "Konfiguréiert Ären API Schlëssel an den Astellungen fir unzefänken", + "carriers": { + "dhl": "DHL", + "ups": "UPS", + "fedex": "FedEx", + "dpd": "DPD", + "gls": "GLS", + "post_luxembourg": "Post Lëtzebuerg", + "other": "Anerer" } }, - "export": { - "title": "Produkter exportéieren", - "subtitle": "Produkter am Marketplace-Format exportéieren", - "format": "Format", - "format_csv": "CSV", - "format_xml": "XML", - "download": "Export eroflueden" - }, "messages": { - "import_started": "Import erfollegräich gestart", - "import_completed": "Import ofgeschloss", - "import_failed": "Import feelgeschloen", - "credentials_saved": "Umeldedaten erfollegräich gespäichert", - "sync_started": "Synchronisatioun gestart", - "sync_completed": "Synchronisatioun ofgeschloss", - "sync_failed": "Synchronisatioun feelgeschloen", - "export_ready": "Export prett zum Eroflueden", - "error_loading": "Feeler beim Lueden vun den Daten" - }, - "filters": { - "all_marketplaces": "All Marketplacen", - "all_vendors": "All Verkeefer", - "search_placeholder": "Produkter sichen..." + "no_error_details_available": "No error details available", + "failed_to_load_error_details": "Failed to load error details", + "copied_to_clipboard": "Copied to clipboard", + "failed_to_copy_to_clipboard": "Failed to copy to clipboard" } } diff --git a/app/modules/marketplace/routes/pages/admin.py b/app/modules/marketplace/routes/pages/admin.py index a9ab05c7..1bbf9c35 100644 --- a/app/modules/marketplace/routes/pages/admin.py +++ b/app/modules/marketplace/routes/pages/admin.py @@ -18,8 +18,8 @@ from app.api.deps import get_db, require_menu_access from app.core.config import settings from app.modules.core.utils.page_context import get_admin_context from app.templates_config import templates -from models.database.admin_menu_config import FrontendType -from models.database.user import User +from app.modules.enums import FrontendType +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/marketplace/routes/pages/vendor.py b/app/modules/marketplace/routes/pages/vendor.py index 5e2370bf..7ecda13a 100644 --- a/app/modules/marketplace/routes/pages/vendor.py +++ b/app/modules/marketplace/routes/pages/vendor.py @@ -17,7 +17,7 @@ from app.api.deps import get_current_vendor_from_cookie_or_header, get_db from app.modules.core.utils.page_context import get_vendor_context from app.modules.marketplace.services.onboarding_service import OnboardingService from app.templates_config import templates -from models.database.user import User +from app.modules.tenancy.models import User router = APIRouter() diff --git a/app/modules/marketplace/services/letzshop/order_service.py b/app/modules/marketplace/services/letzshop/order_service.py index 31f8a506..53bb19e8 100644 --- a/app/modules/marketplace/services/letzshop/order_service.py +++ b/app/modules/marketplace/services/letzshop/order_service.py @@ -25,7 +25,7 @@ from app.modules.marketplace.models import ( ) from app.modules.orders.models import Order, OrderItem from app.modules.catalog.models import Product -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) @@ -627,7 +627,7 @@ class LetzshopOrderService: vendor_lookup = {vendor_id: (vendor.name if vendor else None, vendor.vendor_code if vendor else None)} else: # Build lookup for all vendors when showing all jobs - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor vendors = self.db.query(Vendor.id, Vendor.name, Vendor.vendor_code).all() vendor_lookup = {v.id: (v.name, v.vendor_code) for v in vendors} diff --git a/app/modules/marketplace/services/letzshop/vendor_sync_service.py b/app/modules/marketplace/services/letzshop/vendor_sync_service.py index b9066b26..16545204 100644 --- a/app/modules/marketplace/services/letzshop/vendor_sync_service.py +++ b/app/modules/marketplace/services/letzshop/vendor_sync_service.py @@ -437,9 +437,9 @@ class LetzshopVendorSyncService: from sqlalchemy import func from app.modules.tenancy.services.admin_service import admin_service - from models.database.company import Company - from models.database.vendor import Vendor - from models.schema.vendor import VendorCreate + from app.modules.tenancy.models import Company + from app.modules.tenancy.models import Vendor + from app.modules.tenancy.schemas.vendor import VendorCreate # Get cache entry cache_entry = self.get_cached_vendor(letzshop_slug) diff --git a/app/modules/marketplace/services/marketplace_import_job_service.py b/app/modules/marketplace/services/marketplace_import_job_service.py index 9b99cd75..35dfe60b 100644 --- a/app/modules/marketplace/services/marketplace_import_job_service.py +++ b/app/modules/marketplace/services/marketplace_import_job_service.py @@ -12,8 +12,8 @@ from app.modules.marketplace.models import ( MarketplaceImportError, MarketplaceImportJob, ) -from models.database.user import User -from models.database.vendor import Vendor +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Vendor from app.modules.marketplace.schemas import ( AdminMarketplaceImportJobResponse, MarketplaceImportJobRequest, diff --git a/app/modules/marketplace/services/marketplace_product_service.py b/app/modules/marketplace/services/marketplace_product_service.py index c3e90ac9..0312b1e1 100644 --- a/app/modules/marketplace/services/marketplace_product_service.py +++ b/app/modules/marketplace/services/marketplace_product_service.py @@ -861,7 +861,7 @@ class MarketplaceProductService: """ from app.modules.catalog.models import Product from app.modules.catalog.models import ProductTranslation - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first() if not vendor: diff --git a/app/modules/marketplace/services/onboarding_service.py b/app/modules/marketplace/services/onboarding_service.py index 834f093a..c8a64685 100644 --- a/app/modules/marketplace/services/onboarding_service.py +++ b/app/modules/marketplace/services/onboarding_service.py @@ -31,7 +31,7 @@ from app.modules.marketplace.models import ( OnboardingStep, VendorOnboarding, ) -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/marketplace/services/platform_signup_service.py b/app/modules/marketplace/services/platform_signup_service.py index 51011a38..31e4fc27 100644 --- a/app/modules/marketplace/services/platform_signup_service.py +++ b/app/modules/marketplace/services/platform_signup_service.py @@ -26,15 +26,15 @@ from app.modules.messaging.services.email_service import EmailService from app.modules.marketplace.services.onboarding_service import OnboardingService from app.modules.billing.services.stripe_service import stripe_service from middleware.auth import AuthManager -from models.database.company import Company +from app.modules.tenancy.models import Company from app.modules.billing.models import ( SubscriptionStatus, TierCode, TIER_LIMITS, VendorSubscription, ) -from models.database.user import User -from models.database.vendor import Vendor, VendorUser, VendorUserType +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Vendor, VendorUser, VendorUserType logger = logging.getLogger(__name__) diff --git a/app/modules/marketplace/static/admin/js/marketplace-product-detail.js b/app/modules/marketplace/static/admin/js/marketplace-product-detail.js index 7c8f86d6..d9ce81b1 100644 --- a/app/modules/marketplace/static/admin/js/marketplace-product-detail.js +++ b/app/modules/marketplace/static/admin/js/marketplace-product-detail.js @@ -44,6 +44,9 @@ function adminMarketplaceProductDetail() { targetVendors: [], async init() { + // Load i18n translations + await I18n.loadModule('marketplace'); + adminMarketplaceProductDetailLog.info('Marketplace Product Detail init() called, ID:', this.productId); // Guard against multiple initialization @@ -219,10 +222,10 @@ function adminMarketplaceProductDetail() { if (!text) return; try { await navigator.clipboard.writeText(text); - Utils.showToast('Copied to clipboard', 'success'); + Utils.showToast(I18n.t('marketplace.messages.copied_to_clipboard'), 'success'); } catch (err) { adminMarketplaceProductDetailLog.error('Failed to copy to clipboard:', err); - Utils.showToast('Failed to copy to clipboard', 'error'); + Utils.showToast(I18n.t('marketplace.messages.failed_to_copy_to_clipboard'), 'error'); } } }; diff --git a/app/modules/marketplace/tasks/export_tasks.py b/app/modules/marketplace/tasks/export_tasks.py index 2291758d..4e0418f3 100644 --- a/app/modules/marketplace/tasks/export_tasks.py +++ b/app/modules/marketplace/tasks/export_tasks.py @@ -11,7 +11,7 @@ from pathlib import Path from app.core.celery_config import celery_app from app.modules.task_base import ModuleTask -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/marketplace/tasks/import_tasks.py b/app/modules/marketplace/tasks/import_tasks.py index 2957b23c..7a415954 100644 --- a/app/modules/marketplace/tasks/import_tasks.py +++ b/app/modules/marketplace/tasks/import_tasks.py @@ -22,7 +22,7 @@ from app.modules.marketplace.services.letzshop import ( ) from app.modules.task_base import ModuleTask from app.utils.csv_processor import CSVProcessor -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/marketplace/templates/marketplace/public/find-shop.html b/app/modules/marketplace/templates/marketplace/public/find-shop.html index fa8ffcc1..e263e737 100644 --- a/app/modules/marketplace/templates/marketplace/public/find-shop.html +++ b/app/modules/marketplace/templates/marketplace/public/find-shop.html @@ -2,7 +2,7 @@ {# Letzshop Vendor Finder Page #} {% extends "public/base.html" %} -{% block title %}{{ _("platform.find_shop.title") }} - Wizamart{% endblock %} +{% block title %}{{ _("cms.platform.find_shop.title") }} - Wizamart{% endblock %} {% block content %}- {{ _("platform.find_shop.subtitle") }} + {{ _("cms.platform.find_shop.subtitle") }}
{{ _("platform.find_shop.found") }}
+{{ _("cms.platform.find_shop.found") }}
- {{ _("platform.find_shop.claim_button") }} + {{ _("cms.platform.find_shop.claim_button") }}- {{ _("platform.find_shop.no_account_yet") }} + {{ _("cms.platform.find_shop.no_account_yet") }}
- {{ _("platform.footer.tagline") }} + {{ _("cms.platform.footer.tagline") }}
- {{ _("platform.footer.copyright", year=2025) }} + {{ _("cms.platform.footer.copyright", year=2025) }}