From d7a0ff8818c3b0108769ecf7ae64dce8b3412f59 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sun, 1 Feb 2026 21:02:56 +0100 Subject: [PATCH] refactor: complete module-driven architecture migration This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 --- .architecture-rules/module.yaml | 4 +- alembic/env.py | 10 +- app/api/deps.py | 8 +- app/api/v1/admin/__init__.py | 47 +- app/config/menu_registry.py | 2 +- app/core/feature_gate.py | 2 +- app/core/logging.py | 2 +- app/core/theme_presets.py | 2 +- app/modules/analytics/definition.py | 25 +- app/modules/analytics/routes/api/vendor.py | 2 +- app/modules/analytics/routes/pages/admin.py | 4 +- app/modules/analytics/routes/pages/vendor.py | 4 +- .../analytics/services/stats_service.py | 4 +- .../analytics/services/usage_service.py | 2 +- app/modules/base.py | 145 +++++- app/modules/billing/definition.py | 70 ++- app/modules/billing/locales/de.json | 12 +- app/modules/billing/locales/en.json | 12 +- app/modules/billing/locales/fr.json | 12 +- app/modules/billing/locales/lb.json | 12 +- app/modules/billing/routes/api/admin.py | 2 +- app/modules/billing/routes/api/vendor.py | 2 +- app/modules/billing/routes/pages/admin.py | 4 +- app/modules/billing/routes/pages/vendor.py | 2 +- .../services/admin_subscription_service.py | 2 +- .../billing/services/billing_service.py | 2 +- .../services/capacity_forecast_service.py | 2 +- .../billing/services/stripe_service.py | 4 +- .../billing/services/subscription_service.py | 2 +- .../billing/static/vendor/js/billing.js | 23 +- .../templates/billing/public/pricing.html | 36 +- .../billing/public/signup-success.html | 22 +- app/modules/cart/routes/api/storefront.py | 2 +- app/modules/catalog/definition.py | 24 +- app/modules/catalog/locales/de.json | 97 ++-- app/modules/catalog/locales/fr.json | 95 ++-- app/modules/catalog/locales/lb.json | 97 ++-- app/modules/catalog/routes/api/storefront.py | 2 +- app/modules/catalog/routes/pages/admin.py | 4 +- app/modules/catalog/routes/pages/vendor.py | 2 +- .../services/vendor_product_service.py | 2 +- .../catalog/static/admin/js/product-create.js | 23 +- .../catalog/static/admin/js/product-edit.js | 21 +- .../catalog/static/admin/js/products.js | 5 +- .../catalog/static/vendor/js/products.js | 35 +- app/modules/checkout/locales/de.json | 54 +- app/modules/checkout/locales/en.json | 56 +- app/modules/checkout/locales/fr.json | 54 +- app/modules/checkout/locales/lb.json | 54 +- app/modules/checkout/routes/api/storefront.py | 2 +- app/modules/cms/definition.py | 55 +- app/modules/cms/locales/de.json | 311 ++++++----- app/modules/cms/locales/fr.json | 311 ++++++----- app/modules/cms/locales/lb.json | 311 ++++++----- app/modules/cms/models/__init__.py | 17 +- .../modules/cms/models}/media.py | 7 +- .../modules/cms/models}/vendor_theme.py | 5 +- .../cms/routes/api/admin_content_pages.py | 2 +- app/modules/cms/routes/api/admin_images.py | 2 +- app/modules/cms/routes/api/admin_media.py | 2 +- .../cms/routes/api/admin_vendor_themes.py | 2 +- .../cms/routes/api/vendor_content_pages.py | 2 +- app/modules/cms/routes/api/vendor_media.py | 2 +- app/modules/cms/routes/pages/admin.py | 4 +- app/modules/cms/routes/pages/vendor.py | 4 +- app/modules/cms/schemas/__init__.py | 67 +++ .../modules/cms/schemas}/image.py | 2 +- .../modules/cms/schemas}/media.py | 2 +- .../modules/cms/schemas}/vendor_theme.py | 2 +- app/modules/cms/services/media_service.py | 2 +- .../services/vendor_email_settings_service.py | 7 +- .../cms/services/vendor_theme_service.py | 6 +- .../cms/static/admin/js/content-pages.js | 5 +- .../cms/public/homepage-wizamart.html | 74 +-- app/modules/core/definition.py | 94 +++- app/modules/core/exceptions.py | 34 ++ app/modules/core/locales/de.json | 71 +++ app/modules/core/locales/fr.json | 71 +++ app/modules/core/locales/lb.json | 71 +++ app/modules/core/models/__init__.py | 18 + .../modules/core/models}/admin_menu_config.py | 8 +- app/modules/core/routes/api/admin.py | 3 + .../core/routes/api/admin_menu_config.py} | 4 +- app/modules/core/routes/api/admin_settings.py | 4 +- app/modules/core/routes/pages/admin.py | 4 +- app/modules/core/routes/pages/vendor.py | 2 +- app/modules/core/schemas/__init__.py | 0 app/modules/core/services/__init__.py | 13 +- .../core/services/admin_settings_service.py | 4 +- app/modules/core/services/auth_service.py | 4 +- .../core/services/menu_discovery_service.py | 446 ++++++++++++++++ app/modules/core/services/menu_service.py | 50 +- .../services/platform_settings_service.py | 2 +- app/modules/core/static/admin/js/dashboard.js | 7 +- app/modules/core/utils/page_context.py | 4 +- app/modules/customers/definition.py | 41 +- app/modules/customers/locales/de.json | 22 +- app/modules/customers/locales/en.json | 27 +- app/modules/customers/locales/fr.json | 22 +- app/modules/customers/locales/lb.json | 22 +- app/modules/customers/routes/pages/admin.py | 4 +- app/modules/customers/routes/pages/vendor.py | 2 +- .../services/admin_customer_service.py | 2 +- .../customers/services/customer_service.py | 2 +- .../customers/static/admin/js/customers.js | 5 +- .../customers/static/vendor/js/customers.js | 7 +- app/modules/dev_tools/definition.py | 32 +- app/modules/dev_tools/locales/de.json | 13 +- app/modules/dev_tools/locales/en.json | 13 +- app/modules/dev_tools/locales/fr.json | 13 +- app/modules/dev_tools/locales/lb.json | 13 +- app/modules/dev_tools/routes/pages/admin.py | 4 +- .../dev_tools/static/admin/js/components.js | 15 +- .../dev_tools/static/admin/js/icons-page.js | 7 +- .../static/admin/js/testing-dashboard.js | 5 +- app/modules/enums.py | 9 +- app/modules/inventory/definition.py | 48 +- app/modules/inventory/locales/de.json | 23 +- app/modules/inventory/locales/en.json | 32 +- app/modules/inventory/locales/fr.json | 23 +- app/modules/inventory/locales/lb.json | 23 +- app/modules/inventory/routes/pages/admin.py | 4 +- app/modules/inventory/routes/pages/vendor.py | 2 +- .../inventory/services/inventory_service.py | 2 +- .../services/inventory_transaction_service.py | 2 +- .../inventory/static/admin/js/inventory.js | 27 +- .../inventory/static/vendor/js/inventory.js | 18 +- app/modules/loyalty/definition.py | 62 ++- app/modules/loyalty/routes/api/admin.py | 2 +- app/modules/loyalty/routes/api/public.py | 2 +- app/modules/loyalty/routes/api/vendor.py | 2 +- app/modules/marketplace/definition.py | 56 +- app/modules/marketplace/locales/de.json | 163 ++---- app/modules/marketplace/locales/fr.json | 165 ++---- app/modules/marketplace/locales/lb.json | 165 ++---- app/modules/marketplace/routes/pages/admin.py | 4 +- .../marketplace/routes/pages/vendor.py | 2 +- .../services/letzshop/order_service.py | 4 +- .../services/letzshop/vendor_sync_service.py | 6 +- .../marketplace_import_job_service.py | 4 +- .../services/marketplace_product_service.py | 2 +- .../services/onboarding_service.py | 2 +- .../services/platform_signup_service.py | 6 +- .../admin/js/marketplace-product-detail.js | 7 +- app/modules/marketplace/tasks/export_tasks.py | 2 +- app/modules/marketplace/tasks/import_tasks.py | 2 +- .../marketplace/public/find-shop.html | 34 +- app/modules/messaging/definition.py | 93 +++- app/modules/messaging/locales/de.json | 41 +- app/modules/messaging/locales/fr.json | 41 +- app/modules/messaging/locales/lb.json | 41 +- app/modules/messaging/models/__init__.py | 29 +- .../modules/messaging/models}/email.py | 9 +- .../models}/vendor_email_settings.py | 5 +- .../models}/vendor_email_template.py | 9 +- .../routes/api/admin_notifications.py | 2 +- .../messaging/routes/api/storefront.py | 4 +- app/modules/messaging/routes/pages/admin.py | 4 +- app/modules/messaging/routes/pages/vendor.py | 2 +- app/modules/messaging/schemas/__init__.py | 31 ++ .../modules/messaging/schemas}/email.py | 2 +- .../services/admin_notification_service.py | 4 +- .../messaging/services/email_service.py | 10 +- .../services/email_template_service.py | 4 +- .../messaging/services/messaging_service.py | 4 +- .../static/admin/js/email-templates.js | 21 +- app/modules/monitoring/definition.py | 69 ++- app/modules/monitoring/models/__init__.py | 2 +- .../monitoring/routes/api/admin_audit.py | 2 +- .../monitoring/routes/api/admin_logs.py | 4 +- app/modules/monitoring/routes/pages/admin.py | 4 +- .../services/admin_audit_service.py | 6 +- .../monitoring/services/log_service.py | 4 +- .../services/platform_health_service.py | 4 +- app/modules/orders/definition.py | 42 +- app/modules/orders/locales/de.json | 51 +- app/modules/orders/locales/en.json | 51 +- app/modules/orders/locales/fr.json | 51 +- app/modules/orders/locales/lb.json | 51 +- app/modules/orders/routes/pages/admin.py | 4 +- app/modules/orders/routes/pages/vendor.py | 2 +- .../orders/services/invoice_service.py | 2 +- app/modules/orders/services/order_service.py | 2 +- app/modules/orders/static/admin/js/orders.js | 11 +- .../orders/static/vendor/js/order-detail.js | 9 +- app/modules/orders/static/vendor/js/orders.js | 5 +- app/modules/payments/definition.py | 2 +- app/modules/registry.py | 2 +- app/modules/service.py | 6 +- app/modules/tenancy/definition.py | 83 ++- app/modules/tenancy/locales/de.json | 81 +++ app/modules/tenancy/locales/fr.json | 81 +++ app/modules/tenancy/locales/lb.json | 81 +++ app/modules/tenancy/models/__init__.py | 56 +- .../modules/tenancy/models}/admin.py | 15 +- .../modules/tenancy/models}/admin_platform.py | 5 +- .../modules/tenancy/models}/company.py | 5 +- .../modules/tenancy/models}/platform.py | 5 +- .../tenancy/models}/platform_module.py | 5 +- .../modules/tenancy/models}/user.py | 7 +- .../modules/tenancy/models}/vendor.py | 7 +- .../modules/tenancy/models}/vendor_domain.py | 17 +- .../tenancy/models}/vendor_platform.py | 5 +- app/modules/tenancy/routes/api/admin.py | 6 + app/modules/tenancy/routes/api/admin_auth.py | 2 +- .../tenancy/routes/api/admin_companies.py | 2 +- .../routes/api/admin_module_config.py} | 2 +- .../tenancy/routes/api/admin_modules.py} | 4 +- app/modules/tenancy/routes/api/admin_users.py | 2 +- .../routes/api/admin_vendor_domains.py | 2 +- .../tenancy/routes/api/admin_vendors.py | 2 +- app/modules/tenancy/routes/api/vendor.py | 2 +- .../tenancy/routes/api/vendor_profile.py | 2 +- app/modules/tenancy/routes/api/vendor_team.py | 2 +- app/modules/tenancy/routes/pages/admin.py | 4 +- app/modules/tenancy/routes/pages/vendor.py | 2 +- app/modules/tenancy/schemas/__init__.py | 208 +++++++- .../modules/tenancy/schemas}/admin.py | 2 +- .../modules/tenancy/schemas}/company.py | 2 +- .../modules/tenancy/schemas}/team.py | 2 +- .../modules/tenancy/schemas}/vendor.py | 2 +- .../modules/tenancy/schemas}/vendor_domain.py | 2 +- .../services/admin_platform_service.py | 6 +- app/modules/tenancy/services/admin_service.py | 12 +- .../tenancy/services/company_service.py | 6 +- .../tenancy/services/platform_service.py | 4 +- app/modules/tenancy/services/team_service.py | 4 +- .../tenancy/services/vendor_domain_service.py | 6 +- .../tenancy/services/vendor_service.py | 16 +- .../tenancy/services/vendor_team_service.py | 4 +- .../static/admin/js/admin-user-detail.js | 15 +- .../static/admin/js/admin-user-edit.js | 21 +- .../tenancy/static/admin/js/admin-users.js | 9 +- .../tenancy/static/admin/js/company-detail.js | 11 +- .../tenancy/static/admin/js/company-edit.js | 15 +- .../tenancy/static/admin/js/user-create.js | 5 +- .../tenancy/static/admin/js/user-detail.js | 11 +- .../tenancy/static/admin/js/user-edit.js | 13 +- app/modules/tenancy/static/admin/js/users.js | 9 +- .../tenancy/static/admin/js/vendor-detail.js | 15 +- .../tenancy/static/admin/js/vendor-edit.js | 15 +- .../tenancy/static/admin/js/vendors.js | 9 +- .../tenancy/static/vendor/js/profile.js | 7 +- .../tenancy/static/vendor/js/settings.js | 25 +- app/modules/tenancy/static/vendor/js/team.js | 11 +- app/templates/admin/base.html | 10 + app/templates/public/base.html | 28 +- app/templates/storefront/base.html | 10 + app/templates/vendor/base.html | 10 + docs/architecture/menu-management.md | 402 +++++++++------ docs/architecture/models-structure.md | 193 +++---- main.py | 12 +- middleware/auth.py | 2 +- middleware/platform_context.py | 2 +- middleware/theme_context.py | 2 +- middleware/vendor_context.py | 6 +- models/__init__.py | 26 +- models/database/__init__.py | 266 +--------- models/schema/__init__.py | 21 +- scripts/add_i18n_module_loading.py | 127 +++++ scripts/create_dummy_letzshop_order.py | 4 +- scripts/create_landing_page.py | 2 +- scripts/debug_historical_import.py | 163 ------ scripts/init_log_settings.py | 2 +- scripts/init_production.py | 4 +- scripts/migrate_js_i18n.py | 196 +++++++ scripts/seed_demo.py | 12 +- scripts/seed_email_templates.py | 2 +- scripts/test_logging_system.py | 2 +- scripts/validate_architecture.py | 5 +- scripts/verify_setup.py | 4 +- static/locales/de.json | 481 ------------------ static/locales/fr.json | 481 ------------------ static/locales/lb.json | 481 ------------------ static/shared/js/i18n.js | 158 ++++++ tests/fixtures/admin_platform_fixtures.py | 6 +- tests/fixtures/auth_fixtures.py | 2 +- tests/fixtures/module_fixtures.py | 4 +- tests/fixtures/vendor_fixtures.py | 6 +- .../api/v1/admin/test_admin_users.py | 10 +- .../api/v1/admin/test_email_settings.py | 2 +- .../api/v1/modules/test_module_access.py | 2 +- .../api/v1/public/test_letzshop_vendors.py | 6 +- .../integration/api/v1/public/test_signup.py | 6 +- .../api/v1/vendor/test_dashboard.py | 4 +- .../api/v1/vendor/test_email_settings.py | 2 +- tests/integration/middleware/conftest.py | 8 +- .../middleware/test_theme_loading_flow.py | 2 +- tests/unit/middleware/test_auth.py | 2 +- .../models/database/test_admin_platform.py | 4 +- tests/unit/models/database/test_team.py | 2 +- tests/unit/models/database/test_user.py | 2 +- tests/unit/models/database/test_vendor.py | 2 +- tests/unit/models/schema/test_vendor.py | 2 +- .../test_admin_notification_service.py | 2 +- .../services/test_admin_platform_service.py | 14 +- tests/unit/services/test_admin_service.py | 12 +- tests/unit/services/test_auth_service.py | 2 +- .../services/test_content_page_service.py | 4 +- tests/unit/services/test_email_service.py | 2 +- tests/unit/services/test_inventory_service.py | 6 +- .../unit/services/test_marketplace_service.py | 4 +- tests/unit/services/test_module_service.py | 2 +- tests/unit/services/test_team_service.py | 2 +- tests/unit/services/test_usage_service.py | 4 +- .../test_vendor_email_settings_service.py | 3 +- tests/unit/services/test_vendor_service.py | 14 +- 307 files changed, 5536 insertions(+), 3826 deletions(-) rename {models/database => app/modules/cms/models}/media.py (94%) rename {models/database => app/modules/cms/models}/vendor_theme.py (98%) rename {models/schema => app/modules/cms/schemas}/image.py (96%) rename {models/schema => app/modules/cms/schemas}/media.py (99%) rename {models/schema => app/modules/cms/schemas}/vendor_theme.py (98%) create mode 100644 app/modules/core/exceptions.py create mode 100644 app/modules/core/locales/de.json create mode 100644 app/modules/core/locales/fr.json create mode 100644 app/modules/core/locales/lb.json create mode 100644 app/modules/core/models/__init__.py rename {models/database => app/modules/core/models}/admin_menu_config.py (96%) rename app/{api/v1/admin/menu_config.py => modules/core/routes/api/admin_menu_config.py} (99%) create mode 100644 app/modules/core/schemas/__init__.py create mode 100644 app/modules/core/services/menu_discovery_service.py rename {models/database => app/modules/messaging/models}/email.py (98%) rename {models/database => app/modules/messaging/models}/vendor_email_settings.py (98%) rename {models/database => app/modules/messaging/models}/vendor_email_template.py (97%) rename {models/schema => app/modules/messaging/schemas}/email.py (99%) create mode 100644 app/modules/tenancy/locales/de.json create mode 100644 app/modules/tenancy/locales/fr.json create mode 100644 app/modules/tenancy/locales/lb.json rename {models/database => app/modules/tenancy/models}/admin.py (97%) rename {models/database => app/modules/tenancy/models}/admin_platform.py (98%) rename {models/database => app/modules/tenancy/models}/company.py (98%) rename {models/database => app/modules/tenancy/models}/platform.py (99%) rename {models/database => app/modules/tenancy/models}/platform_module.py (98%) rename {models/database => app/modules/tenancy/models}/user.py (98%) rename {models/database => app/modules/tenancy/models}/vendor.py (99%) rename {models/database => app/modules/tenancy/models}/vendor_domain.py (88%) rename {models/database => app/modules/tenancy/models}/vendor_platform.py (98%) rename app/{api/v1/admin/module_config.py => modules/tenancy/routes/api/admin_module_config.py} (99%) rename app/{api/v1/admin/modules.py => modules/tenancy/routes/api/admin_modules.py} (98%) rename {models/schema => app/modules/tenancy/schemas}/admin.py (99%) rename {models/schema => app/modules/tenancy/schemas}/company.py (99%) rename {models/schema => app/modules/tenancy/schemas}/team.py (99%) rename {models/schema => app/modules/tenancy/schemas}/vendor.py (99%) rename {models/schema => app/modules/tenancy/schemas}/vendor_domain.py (98%) create mode 100644 scripts/add_i18n_module_loading.py delete mode 100644 scripts/debug_historical_import.py create mode 100644 scripts/migrate_js_i18n.py create mode 100644 static/shared/js/i18n.js 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 %}
@@ -10,15 +10,15 @@ {# Header #}

- {{ _("platform.pricing.title") }} + {{ _("cms.platform.pricing.title") }}

- {{ _("platform.pricing.trial_note", trial_days=trial_days) }} + {{ _("cms.platform.pricing.trial_note", trial_days=trial_days) }}

{# Billing Toggle #}
- {{ _("platform.pricing.monthly") }} + {{ _("cms.platform.pricing.monthly") }} - {{ _("platform.pricing.annual") }} - {{ _("platform.pricing.save_months") }} + {{ _("cms.platform.pricing.annual") }} + {{ _("cms.platform.pricing.save_months") }}
@@ -40,7 +40,7 @@ {% if tier.is_popular %}
- {{ _("platform.pricing.recommended") }} + {{ _("cms.platform.pricing.recommended") }}
{% endif %} @@ -50,17 +50,17 @@ @@ -71,37 +71,37 @@ - {% if tier.orders_per_month %}{{ _("platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("platform.pricing.unlimited_orders") }}{% endif %} + {% if tier.orders_per_month %}{{ _("cms.platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("cms.platform.pricing.unlimited_orders") }}{% endif %}
  • - {% if tier.products_limit %}{{ _("platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("platform.pricing.unlimited_products") }}{% endif %} + {% if tier.products_limit %}{{ _("cms.platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("cms.platform.pricing.unlimited_products") }}{% endif %}
  • - {% if tier.team_members %}{{ _("platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("platform.pricing.unlimited_team") }}{% endif %} + {% if tier.team_members %}{{ _("cms.platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("cms.platform.pricing.unlimited_team") }}{% endif %}
  • - {{ _("platform.pricing.letzshop_sync") }} + {{ _("cms.platform.pricing.letzshop_sync") }}
  • {% if tier.is_enterprise %} - {{ _("platform.pricing.contact_sales") }} + {{ _("cms.platform.pricing.contact_sales") }} {% else %} - {{ _("platform.pricing.start_trial") }} + {{ _("cms.platform.pricing.start_trial") }} {% endif %}
    @@ -111,7 +111,7 @@ {# Back to Home #} diff --git a/app/modules/billing/templates/billing/public/signup-success.html b/app/modules/billing/templates/billing/public/signup-success.html index c13c8d7d..cf2110b0 100644 --- a/app/modules/billing/templates/billing/public/signup-success.html +++ b/app/modules/billing/templates/billing/public/signup-success.html @@ -2,7 +2,7 @@ {# Signup Success Page #} {% extends "public/base.html" %} -{% block title %}{{ _("platform.success.title") }}{% endblock %} +{% block title %}{{ _("cms.platform.success.title") }}{% endblock %} {% block content %}
    @@ -17,23 +17,23 @@ {# Welcome Message #}

    - {{ _("platform.success.title") }} + {{ _("cms.platform.success.title") }}

    - {{ _("platform.success.subtitle", trial_days=trial_days) }} + {{ _("cms.platform.success.subtitle", trial_days=trial_days) }}

    {# Next Steps #}
    -

    {{ _("platform.success.what_next") }}

    +

    {{ _("cms.platform.success.what_next") }}

    • 1
      - {{ _("platform.success.step_connect") }} {{ _("platform.success.step_connect_desc") }} + {{ _("cms.platform.success.step_connect") }} {{ _("cms.platform.success.step_connect_desc") }}
    • @@ -41,7 +41,7 @@ 2
    - {{ _("platform.success.step_invoicing") }} {{ _("platform.success.step_invoicing_desc") }} + {{ _("cms.platform.success.step_invoicing") }} {{ _("cms.platform.success.step_invoicing_desc") }}
  • @@ -49,7 +49,7 @@ 3
  • - {{ _("platform.success.step_products") }} {{ _("platform.success.step_products_desc") }} + {{ _("cms.platform.success.step_products") }} {{ _("cms.platform.success.step_products_desc") }} @@ -59,7 +59,7 @@ {% if vendor_code %} - {{ _("platform.success.go_to_dashboard") }} + {{ _("cms.platform.success.go_to_dashboard") }} @@ -67,14 +67,14 @@ {% else %} - {{ _("platform.success.login_dashboard") }} + {{ _("cms.platform.success.login_dashboard") }} {% endif %} {# Support Link #}

    - {{ _("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.title") }} + {{ _("cms.platform.hero.title") }}

    {# Subheadline #}

    - {{ _("platform.hero.subtitle") }} + {{ _("cms.platform.hero.subtitle") }}

    {# CTA Buttons #} @@ -68,19 +68,19 @@ {# Section Header #}

    - {{ _("platform.pricing.title") }} + {{ _("cms.platform.pricing.title") }}

    - {{ _("platform.pricing.subtitle", trial_days=trial_days) }} + {{ _("cms.platform.pricing.subtitle", trial_days=trial_days) }}

    {# Billing Toggle #}
    {{ toggle_switch( model='annual', - left_label=_("platform.pricing.monthly"), - right_label=_("platform.pricing.annual"), - right_badge=_("platform.pricing.save_months") + left_label=_("cms.platform.pricing.monthly"), + right_label=_("cms.platform.pricing.annual"), + right_badge=_("cms.platform.pricing.save_months") ) }}
    @@ -95,7 +95,7 @@ {% if tier.is_popular %}
    - {{ _("platform.pricing.most_popular") }} + {{ _("cms.platform.pricing.most_popular") }}
    {% endif %} @@ -108,19 +108,19 @@ @@ -133,28 +133,28 @@ - {% if tier.orders_per_month %}{{ _("platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("platform.pricing.unlimited_orders") }}{% endif %} + {% if tier.orders_per_month %}{{ _("cms.platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("cms.platform.pricing.unlimited_orders") }}{% endif %} {# Products #}
  • - {% if tier.products_limit %}{{ _("platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("platform.pricing.unlimited_products") }}{% endif %} + {% if tier.products_limit %}{{ _("cms.platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("cms.platform.pricing.unlimited_products") }}{% endif %}
  • {# Team Members #}
  • - {% if tier.team_members %}{{ _("platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("platform.pricing.unlimited_team") }}{% endif %} + {% if tier.team_members %}{{ _("cms.platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("cms.platform.pricing.unlimited_team") }}{% endif %}
  • {# Letzshop Sync - always included #}
  • - {{ _("platform.pricing.letzshop_sync") }} + {{ _("cms.platform.pricing.letzshop_sync") }}
  • {# EU VAT Invoicing #}
  • @@ -167,7 +167,7 @@ {% endif %} - {{ _("platform.pricing.eu_vat_invoicing") }} + {{ _("cms.platform.pricing.eu_vat_invoicing") }}
  • {# Analytics Dashboard #}
  • @@ -180,7 +180,7 @@ {% endif %} - {{ _("platform.pricing.analytics_dashboard") }} + {{ _("cms.platform.pricing.analytics_dashboard") }}
  • {# API Access #}
  • @@ -193,7 +193,7 @@ {% endif %} - {{ _("platform.pricing.api_access") }} + {{ _("cms.platform.pricing.api_access") }}
  • {# Multi-channel Integration - Enterprise only #}
  • @@ -206,7 +206,7 @@ {% endif %} - {{ _("platform.pricing.multi_channel") }} + {{ _("cms.platform.pricing.multi_channel") }}
  • @@ -214,14 +214,14 @@ {% if tier.is_enterprise %} - {{ _("platform.pricing.contact_sales") }} + {{ _("cms.platform.pricing.contact_sales") }} {% else %} - {{ _("platform.pricing.start_trial") }} + {{ _("cms.platform.pricing.start_trial") }} {% endif %} @@ -238,10 +238,10 @@ {# Section Header #}

    - {{ _("platform.addons.title") }} + {{ _("cms.platform.addons.title") }}

    - {{ _("platform.addons.subtitle") }} + {{ _("cms.platform.addons.subtitle") }}

    @@ -300,10 +300,10 @@ {# Section Header #}

    - {{ _("platform.find_shop.title") }} + {{ _("cms.platform.find_shop.title") }}

    - {{ _("platform.find_shop.subtitle") }} + {{ _("cms.platform.find_shop.subtitle") }}

    @@ -313,7 +313,7 @@ @@ -342,12 +342,12 @@ @@ -362,7 +362,7 @@ {# Help Text #}

    - {{ _("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.title") }} + {{ _("cms.platform.cta.title") }}

    - {{ _("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 %}
    @@ -10,10 +10,10 @@ {# Header #}

    - {{ _("platform.find_shop.title") }} + {{ _("cms.platform.find_shop.title") }}

    - {{ _("platform.find_shop.subtitle") }} + {{ _("cms.platform.find_shop.subtitle") }}

    @@ -24,7 +24,7 @@ type="text" x-model="searchQuery" @keyup.enter="lookupVendor()" - placeholder="{{ _('platform.find_shop.search_placeholder') }}" + placeholder="{{ _('cms.platform.find_shop.search_placeholder') }}" class="flex-1 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent" />
    {# Examples #}
    - {{ _("platform.find_shop.examples") }} + {{ _("cms.platform.find_shop.examples") }}
    • https://letzshop.lu/vendors/my-shop
    • letzshop.lu/vendors/my-shop
    • @@ -61,7 +61,7 @@
      @@ -117,18 +117,18 @@ {# Help Section #}
      -

      {{ _("platform.find_shop.need_help") }}

      +

      {{ _("cms.platform.find_shop.need_help") }}

      - {{ _("platform.find_shop.no_account_yet") }} + {{ _("cms.platform.find_shop.no_account_yet") }}

      diff --git a/app/modules/messaging/definition.py b/app/modules/messaging/definition.py index 13a666d3..75f04bac 100644 --- a/app/modules/messaging/definition.py +++ b/app/modules/messaging/definition.py @@ -6,8 +6,8 @@ Defines the messaging 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(): @@ -47,6 +47,95 @@ messaging_module = ModuleDefinition( "notifications", # Vendor notifications ], }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="platformAdmin", + label_key="messaging.menu.platform_admin", + icon="chat-bubble-left-right", + order=20, + items=[ + MenuItemDefinition( + id="messages", + label_key="messaging.menu.messages", + icon="chat-bubble-left-right", + route="/admin/messages", + order=30, + ), + ], + ), + MenuSectionDefinition( + id="monitoring", + label_key="messaging.menu.platform_monitoring", + icon="bell", + order=80, + items=[ + MenuItemDefinition( + id="notifications", + label_key="messaging.menu.notifications", + icon="bell", + route="/admin/notifications", + order=40, + ), + ], + ), + MenuSectionDefinition( + id="settings", + label_key="messaging.menu.platform_settings", + icon="mail", + order=900, + items=[ + MenuItemDefinition( + id="email-templates", + label_key="messaging.menu.email_templates", + icon="mail", + route="/admin/email-templates", + order=20, + ), + ], + ), + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="customers", + label_key="messaging.menu.customers", + icon="chat-bubble-left-right", + order=30, + items=[ + MenuItemDefinition( + id="messages", + label_key="messaging.menu.messages", + icon="chat-bubble-left-right", + route="/vendor/{vendor_code}/messages", + order=20, + ), + MenuItemDefinition( + id="notifications", + label_key="messaging.menu.notifications", + icon="bell", + route="/vendor/{vendor_code}/notifications", + order=30, + ), + ], + ), + MenuSectionDefinition( + id="account", + label_key="messaging.menu.account_settings", + icon="mail", + order=900, + items=[ + MenuItemDefinition( + id="email-templates", + label_key="messaging.menu.email_templates", + icon="mail", + route="/vendor/{vendor_code}/email-templates", + order=40, + ), + ], + ), + ], + }, is_core=False, # ========================================================================= # Self-Contained Module Configuration diff --git a/app/modules/messaging/locales/de.json b/app/modules/messaging/locales/de.json index 0967ef42..fe8a1f20 100644 --- a/app/modules/messaging/locales/de.json +++ b/app/modules/messaging/locales/de.json @@ -1 +1,40 @@ -{} +{ + "notifications": { + "title": "Benachrichtigungen", + "mark_read": "Als gelesen markieren", + "mark_all_read": "Alle als gelesen markieren", + "no_notifications": "Keine Benachrichtigungen", + "new_order": "Neue Bestellung", + "order_updated": "Bestellung aktualisiert", + "low_stock": "Warnung bei geringem Bestand", + "import_complete": "Import abgeschlossen", + "import_failed": "Import fehlgeschlagen" + }, + "messages": { + "failed_to_load_template": "Failed to load template", + "template_saved_successfully": "Template saved successfully", + "reverted_to_platform_default": "Reverted to platform default", + "failed_to_load_preview": "Failed to load preview", + "failed_to_send_test_email": "Failed to send test email", + "failed_to_load_conversations": "Failed to load conversations", + "failed_to_load_conversation": "Failed to load conversation", + "conversation_closed": "Conversation closed", + "failed_to_close_conversation": "Failed to close conversation", + "conversation_reopened": "Conversation reopened", + "failed_to_reopen_conversation": "Failed to reopen conversation", + "conversation_created": "Conversation created", + "notification_marked_as_read": "Notification marked as read", + "all_notifications_marked_as_read": "All notifications marked as read", + "notification_deleted": "Notification deleted", + "notification_settings_saved": "Notification settings saved", + "failed_to_load_templates": "Failed to load templates", + "failed_to_load_recipients": "Failed to load recipients", + "failed_to_load_notifications": "Failed to load notifications", + "failed_to_mark_notification_as_read": "Failed to mark notification as read", + "failed_to_mark_all_as_read": "Failed to mark all as read", + "failed_to_delete_notification": "Failed to delete notification", + "failed_to_load_alerts": "Failed to load alerts", + "alert_resolved_successfully": "Alert resolved successfully", + "failed_to_resolve_alert": "Failed to resolve alert" + } +} diff --git a/app/modules/messaging/locales/fr.json b/app/modules/messaging/locales/fr.json index 0967ef42..80091f0a 100644 --- a/app/modules/messaging/locales/fr.json +++ b/app/modules/messaging/locales/fr.json @@ -1 +1,40 @@ -{} +{ + "notifications": { + "title": "Notifications", + "mark_read": "Marquer comme lu", + "mark_all_read": "Tout marquer comme lu", + "no_notifications": "Aucune notification", + "new_order": "Nouvelle commande", + "order_updated": "Commande mise à jour", + "low_stock": "Alerte stock faible", + "import_complete": "Importation terminée", + "import_failed": "Échec de l'importation" + }, + "messages": { + "failed_to_load_template": "Failed to load template", + "template_saved_successfully": "Template saved successfully", + "reverted_to_platform_default": "Reverted to platform default", + "failed_to_load_preview": "Failed to load preview", + "failed_to_send_test_email": "Failed to send test email", + "failed_to_load_conversations": "Failed to load conversations", + "failed_to_load_conversation": "Failed to load conversation", + "conversation_closed": "Conversation closed", + "failed_to_close_conversation": "Failed to close conversation", + "conversation_reopened": "Conversation reopened", + "failed_to_reopen_conversation": "Failed to reopen conversation", + "conversation_created": "Conversation created", + "notification_marked_as_read": "Notification marked as read", + "all_notifications_marked_as_read": "All notifications marked as read", + "notification_deleted": "Notification deleted", + "notification_settings_saved": "Notification settings saved", + "failed_to_load_templates": "Failed to load templates", + "failed_to_load_recipients": "Failed to load recipients", + "failed_to_load_notifications": "Failed to load notifications", + "failed_to_mark_notification_as_read": "Failed to mark notification as read", + "failed_to_mark_all_as_read": "Failed to mark all as read", + "failed_to_delete_notification": "Failed to delete notification", + "failed_to_load_alerts": "Failed to load alerts", + "alert_resolved_successfully": "Alert resolved successfully", + "failed_to_resolve_alert": "Failed to resolve alert" + } +} diff --git a/app/modules/messaging/locales/lb.json b/app/modules/messaging/locales/lb.json index 0967ef42..2e880420 100644 --- a/app/modules/messaging/locales/lb.json +++ b/app/modules/messaging/locales/lb.json @@ -1 +1,40 @@ -{} +{ + "notifications": { + "title": "Notifikatiounen", + "mark_read": "Als gelies markéieren", + "mark_all_read": "Alles als gelies markéieren", + "no_notifications": "Keng Notifikatiounen", + "new_order": "Nei Bestellung", + "order_updated": "Bestellung aktualiséiert", + "low_stock": "Niddreg Lager Alarm", + "import_complete": "Import fäerdeg", + "import_failed": "Import feelgeschloen" + }, + "messages": { + "failed_to_load_template": "Failed to load template", + "template_saved_successfully": "Template saved successfully", + "reverted_to_platform_default": "Reverted to platform default", + "failed_to_load_preview": "Failed to load preview", + "failed_to_send_test_email": "Failed to send test email", + "failed_to_load_conversations": "Failed to load conversations", + "failed_to_load_conversation": "Failed to load conversation", + "conversation_closed": "Conversation closed", + "failed_to_close_conversation": "Failed to close conversation", + "conversation_reopened": "Conversation reopened", + "failed_to_reopen_conversation": "Failed to reopen conversation", + "conversation_created": "Conversation created", + "notification_marked_as_read": "Notification marked as read", + "all_notifications_marked_as_read": "All notifications marked as read", + "notification_deleted": "Notification deleted", + "notification_settings_saved": "Notification settings saved", + "failed_to_load_templates": "Failed to load templates", + "failed_to_load_recipients": "Failed to load recipients", + "failed_to_load_notifications": "Failed to load notifications", + "failed_to_mark_notification_as_read": "Failed to mark notification as read", + "failed_to_mark_all_as_read": "Failed to mark all as read", + "failed_to_delete_notification": "Failed to delete notification", + "failed_to_load_alerts": "Failed to load alerts", + "alert_resolved_successfully": "Alert resolved successfully", + "failed_to_resolve_alert": "Failed to resolve alert" + } +} diff --git a/app/modules/messaging/models/__init__.py b/app/modules/messaging/models/__init__.py index e95a6174..18e5c66e 100644 --- a/app/modules/messaging/models/__init__.py +++ b/app/modules/messaging/models/__init__.py @@ -2,7 +2,10 @@ """ Messaging module database models. -This module contains the canonical implementations of messaging-related models. +This module contains the canonical implementations of messaging-related models: +- Message, Conversation: In-app messaging +- AdminNotification: Admin notifications +- Email templates and settings: Email system """ from app.modules.messaging.models.message import ( @@ -14,13 +17,37 @@ from app.modules.messaging.models.message import ( ParticipantType, ) from app.modules.messaging.models.admin_notification import AdminNotification +from app.modules.messaging.models.email import ( + EmailCategory, + EmailLog, + EmailStatus, + EmailTemplate, +) +from app.modules.messaging.models.vendor_email_settings import ( + EmailProvider, + PREMIUM_EMAIL_PROVIDERS, + VendorEmailSettings, +) +from app.modules.messaging.models.vendor_email_template import VendorEmailTemplate __all__ = [ + # Conversations and messages "Conversation", "ConversationParticipant", "ConversationType", "Message", "MessageAttachment", "ParticipantType", + # Admin notifications "AdminNotification", + # Email templates + "EmailCategory", + "EmailLog", + "EmailStatus", + "EmailTemplate", + # Vendor email settings + "EmailProvider", + "PREMIUM_EMAIL_PROVIDERS", + "VendorEmailSettings", + "VendorEmailTemplate", ] diff --git a/models/database/email.py b/app/modules/messaging/models/email.py similarity index 98% rename from models/database/email.py rename to app/modules/messaging/models/email.py index 0e9407f0..e5bce20b 100644 --- a/models/database/email.py +++ b/app/modules/messaging/models/email.py @@ -1,4 +1,4 @@ -# models/database/email.py +# app/modules/messaging/models/email.py """ Email system database models. @@ -20,7 +20,6 @@ from sqlalchemy import ( Boolean, Column, DateTime, - Enum, ForeignKey, Index, Integer, @@ -30,8 +29,7 @@ from sqlalchemy import ( from sqlalchemy.orm import Session, relationship from app.core.database import Base - -from .base import TimestampMixin +from models.database.base import TimestampMixin class EmailCategory(str, enum.Enum): @@ -304,3 +302,6 @@ class EmailLog(Base, TimestampMixin): """Mark email as opened.""" self.status = EmailStatus.OPENED.value self.opened_at = datetime.utcnow() + + +__all__ = ["EmailCategory", "EmailStatus", "EmailTemplate", "EmailLog"] diff --git a/models/database/vendor_email_settings.py b/app/modules/messaging/models/vendor_email_settings.py similarity index 98% rename from models/database/vendor_email_settings.py rename to app/modules/messaging/models/vendor_email_settings.py index 10894fbf..d0ecd840 100644 --- a/models/database/vendor_email_settings.py +++ b/app/modules/messaging/models/vendor_email_settings.py @@ -1,4 +1,4 @@ -# models/database/vendor_email_settings.py +# app/modules/messaging/models/vendor_email_settings.py """ Vendor Email Settings model for vendor-specific email configuration. @@ -253,3 +253,6 @@ class VendorEmailSettings(Base, TimestampMixin): "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, } + + +__all__ = ["EmailProvider", "PREMIUM_EMAIL_PROVIDERS", "VendorEmailSettings"] diff --git a/models/database/vendor_email_template.py b/app/modules/messaging/models/vendor_email_template.py similarity index 97% rename from models/database/vendor_email_template.py rename to app/modules/messaging/models/vendor_email_template.py index 33e017e3..18458c17 100644 --- a/models/database/vendor_email_template.py +++ b/app/modules/messaging/models/vendor_email_template.py @@ -1,4 +1,4 @@ -# models/database/vendor_email_template.py +# app/modules/messaging/models/vendor_email_template.py """ Vendor email template override model. @@ -11,7 +11,6 @@ from datetime import datetime from sqlalchemy import ( Boolean, Column, - DateTime, ForeignKey, Integer, String, @@ -21,8 +20,7 @@ from sqlalchemy import ( from sqlalchemy.orm import Session, relationship from app.core.database import Base - -from .base import TimestampMixin +from models.database.base import TimestampMixin class VendorEmailTemplate(Base, TimestampMixin): @@ -227,3 +225,6 @@ class VendorEmailTemplate(Base, TimestampMixin): .delete() ) return deleted > 0 + + +__all__ = ["VendorEmailTemplate"] diff --git a/app/modules/messaging/routes/api/admin_notifications.py b/app/modules/messaging/routes/api/admin_notifications.py index 8a447b64..42ff571e 100644 --- a/app/modules/messaging/routes/api/admin_notifications.py +++ b/app/modules/messaging/routes/api/admin_notifications.py @@ -20,7 +20,7 @@ from app.modules.messaging.services.admin_notification_service import ( platform_alert_service, ) from models.schema.auth import UserContext -from models.schema.admin import ( +from app.modules.tenancy.schemas.admin import ( AdminNotificationCreate, AdminNotificationListResponse, AdminNotificationResponse, diff --git a/app/modules/messaging/routes/api/storefront.py b/app/modules/messaging/routes/api/storefront.py index 3bfbccb6..63729d88 100644 --- a/app/modules/messaging/routes/api/storefront.py +++ b/app/modules/messaging/routes/api/storefront.py @@ -487,7 +487,7 @@ def _get_other_participant_name(conversation, customer_id: int) -> str: """Get the name of the other participant (the vendor user).""" for participant in conversation.participants: if participant.participant_type == ParticipantType.VENDOR: - from models.database.user import User + from app.modules.tenancy.models import User user = ( User.query.filter_by(id=participant.participant_id).first() @@ -514,7 +514,7 @@ def _get_sender_name(message) -> str: return f"{customer.first_name} {customer.last_name}" return "Customer" elif message.sender_type == ParticipantType.VENDOR: - from models.database.user import User + from app.modules.tenancy.models import User user = ( User.query.filter_by(id=message.sender_id).first() diff --git a/app/modules/messaging/routes/pages/admin.py b/app/modules/messaging/routes/pages/admin.py index df86af0e..be660966 100644 --- a/app/modules/messaging/routes/pages/admin.py +++ b/app/modules/messaging/routes/pages/admin.py @@ -16,8 +16,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/messaging/routes/pages/vendor.py b/app/modules/messaging/routes/pages/vendor.py index 459cefee..685d60b5 100644 --- a/app/modules/messaging/routes/pages/vendor.py +++ b/app/modules/messaging/routes/pages/vendor.py @@ -15,7 +15,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/messaging/schemas/__init__.py b/app/modules/messaging/schemas/__init__.py index 6f30d2cd..baf5bb2d 100644 --- a/app/modules/messaging/schemas/__init__.py +++ b/app/modules/messaging/schemas/__init__.py @@ -57,6 +57,23 @@ from app.modules.messaging.schemas.notification import ( AlertStatisticsResponse, ) +# Email template schemas +from app.modules.messaging.schemas.email import ( + EmailPreviewRequest, + EmailPreviewResponse, + EmailTemplateBase, + EmailTemplateCreate, + EmailTemplateResponse, + EmailTemplateSummary, + EmailTemplateUpdate, + EmailTemplateWithOverrideStatus, + EmailTestRequest, + EmailTestResponse, + VendorEmailTemplateCreate, + VendorEmailTemplateResponse, + VendorEmailTemplateUpdate, +) + __all__ = [ # Attachment schemas "AttachmentResponse", @@ -104,4 +121,18 @@ __all__ = [ "TestNotificationRequest", # Alert statistics "AlertStatisticsResponse", + # Email template schemas + "EmailPreviewRequest", + "EmailPreviewResponse", + "EmailTemplateBase", + "EmailTemplateCreate", + "EmailTemplateResponse", + "EmailTemplateSummary", + "EmailTemplateUpdate", + "EmailTemplateWithOverrideStatus", + "EmailTestRequest", + "EmailTestResponse", + "VendorEmailTemplateCreate", + "VendorEmailTemplateResponse", + "VendorEmailTemplateUpdate", ] diff --git a/models/schema/email.py b/app/modules/messaging/schemas/email.py similarity index 99% rename from models/schema/email.py rename to app/modules/messaging/schemas/email.py index edfd248f..82752633 100644 --- a/models/schema/email.py +++ b/app/modules/messaging/schemas/email.py @@ -1,4 +1,4 @@ -# models/schema/email.py +# app/modules/messaging/schemas/email.py """ Email template Pydantic schemas for API responses and requests. diff --git a/app/modules/messaging/services/admin_notification_service.py b/app/modules/messaging/services/admin_notification_service.py index 8491432d..c611b0b7 100644 --- a/app/modules/messaging/services/admin_notification_service.py +++ b/app/modules/messaging/services/admin_notification_service.py @@ -16,8 +16,8 @@ from sqlalchemy import and_, case, func from sqlalchemy.orm import Session from app.modules.messaging.models.admin_notification import AdminNotification -from models.database.admin import PlatformAlert -from models.schema.admin import AdminNotificationCreate, PlatformAlertCreate +from app.modules.tenancy.models import PlatformAlert +from app.modules.tenancy.schemas.admin import AdminNotificationCreate, PlatformAlertCreate logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/services/email_service.py b/app/modules/messaging/services/email_service.py index 9ab74a28..25b27a0d 100644 --- a/app/modules/messaging/services/email_service.py +++ b/app/modules/messaging/services/email_service.py @@ -41,8 +41,8 @@ from jinja2 import Environment, BaseLoader from sqlalchemy.orm import Session from app.core.config import settings -from models.database.email import EmailLog, EmailStatus, EmailTemplate -from models.database.vendor_email_template import VendorEmailTemplate +from app.modules.messaging.models import EmailLog, EmailStatus, EmailTemplate +from app.modules.messaging.models import VendorEmailTemplate logger = logging.getLogger(__name__) @@ -368,7 +368,7 @@ def get_platform_email_config(db: Session) -> dict: Returns: Dictionary with all email configuration values """ - from models.database.admin import AdminSetting + from app.modules.tenancy.models import AdminSetting def get_db_setting(key: str) -> str | None: setting = db.query(AdminSetting).filter(AdminSetting.key == key).first() @@ -1002,7 +1002,7 @@ class EmailService: def _get_vendor(self, vendor_id: int): """Get vendor with caching.""" if vendor_id not in self._vendor_cache: - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor self._vendor_cache[vendor_id] = ( self.db.query(Vendor).filter(Vendor.id == vendor_id).first() @@ -1026,7 +1026,7 @@ class EmailService: def _get_vendor_email_settings(self, vendor_id: int): """Get vendor email settings with caching.""" if vendor_id not in self._vendor_email_settings_cache: - from models.database.vendor_email_settings import VendorEmailSettings + from app.modules.messaging.models import VendorEmailSettings self._vendor_email_settings_cache[vendor_id] = ( self.db.query(VendorEmailSettings) diff --git a/app/modules/messaging/services/email_template_service.py b/app/modules/messaging/services/email_template_service.py index 9ea2980c..e1dabccd 100644 --- a/app/modules/messaging/services/email_template_service.py +++ b/app/modules/messaging/services/email_template_service.py @@ -24,8 +24,8 @@ from app.exceptions.base import ( ResourceNotFoundException, ValidationException, ) -from models.database.email import EmailCategory, EmailLog, EmailTemplate -from models.database.vendor_email_template import VendorEmailTemplate +from app.modules.messaging.models import EmailCategory, EmailLog, EmailTemplate +from app.modules.messaging.models import VendorEmailTemplate logger = logging.getLogger(__name__) diff --git a/app/modules/messaging/services/messaging_service.py b/app/modules/messaging/services/messaging_service.py index cc61ad02..074effe3 100644 --- a/app/modules/messaging/services/messaging_service.py +++ b/app/modules/messaging/services/messaging_service.py @@ -26,7 +26,7 @@ from app.modules.messaging.models.message import ( ParticipantType, ) from app.modules.customers.models.customer import Customer -from models.database.user import User +from app.modules.tenancy.models import User logger = logging.getLogger(__name__) @@ -592,7 +592,7 @@ class MessagingService: Returns: Tuple of (recipients list, total count) """ - from models.database.vendor import VendorUser + from app.modules.tenancy.models import VendorUser query = ( db.query(User, VendorUser) diff --git a/app/modules/messaging/static/admin/js/email-templates.js b/app/modules/messaging/static/admin/js/email-templates.js index 27d1874c..27e8298c 100644 --- a/app/modules/messaging/static/admin/js/email-templates.js +++ b/app/modules/messaging/static/admin/js/email-templates.js @@ -51,6 +51,9 @@ function emailTemplatesPage() { // Lifecycle async init() { + // Load i18n translations + await I18n.loadModule('messaging'); + if (window._adminEmailTemplatesInitialized) return; window._adminEmailTemplatesInitialized = true; @@ -70,7 +73,7 @@ function emailTemplatesPage() { this.categories = categoriesData.categories || []; } catch (error) { emailTemplatesLog.error('Failed to load email templates:', error); - Utils.showToast('Failed to load templates', 'error'); + Utils.showToast(I18n.t('messaging.messages.failed_to_load_templates'), 'error'); } finally { this.loading = false; } @@ -122,10 +125,10 @@ function emailTemplatesPage() { variables: [], required_variables: [] }; - Utils.showToast(`No template for ${this.editLanguage.toUpperCase()} - create one by saving`, 'info'); + Utils.showToast(I18n.t('messaging.messages.no_template_for_language', { language: this.editLanguage.toUpperCase() }), 'info'); } else { emailTemplatesLog.error('Failed to load template:', error); - Utils.showToast('Failed to load template', 'error'); + Utils.showToast(I18n.t('messaging.messages.failed_to_load_template'), 'error'); } } finally { this.loadingTemplate = false; @@ -158,12 +161,12 @@ function emailTemplatesPage() { } ); - Utils.showToast('Template saved successfully', 'success'); + Utils.showToast(I18n.t('messaging.messages.template_saved_successfully'), 'success'); // Refresh templates list await this.loadData(); } catch (error) { emailTemplatesLog.error('Failed to save template:', error); - Utils.showToast(error.detail || 'Failed to save template', 'error'); + Utils.showToast(error.detail || I18n.t('messaging.messages.failed_to_save_template'), 'error'); } finally { this.saving = false; } @@ -188,7 +191,7 @@ function emailTemplatesPage() { this.showPreviewModal = true; } catch (error) { emailTemplatesLog.error('Failed to preview template:', error); - Utils.showToast('Failed to load preview', 'error'); + Utils.showToast(I18n.t('messaging.messages.failed_to_load_preview'), 'error'); } }, @@ -250,15 +253,15 @@ function emailTemplatesPage() { ); if (result.success) { - Utils.showToast(`Test email sent to ${this.testEmailAddress}`, 'success'); + Utils.showToast(I18n.t('messaging.messages.test_email_sent', { email: this.testEmailAddress }), 'success'); this.showTestEmailModal = false; this.testEmailAddress = ''; } else { - Utils.showToast(result.message || 'Failed to send test email', 'error'); + Utils.showToast(result.message || I18n.t('messaging.messages.failed_to_send_test_email'), 'error'); } } catch (error) { emailTemplatesLog.error('Failed to send test email:', error); - Utils.showToast('Failed to send test email', 'error'); + Utils.showToast(I18n.t('messaging.messages.failed_to_send_test_email'), 'error'); } finally { this.sendingTest = false; } diff --git a/app/modules/monitoring/definition.py b/app/modules/monitoring/definition.py index db51b3a4..4ece4d90 100644 --- a/app/modules/monitoring/definition.py +++ b/app/modules/monitoring/definition.py @@ -6,8 +6,8 @@ Defines the monitoring 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(): @@ -44,6 +44,71 @@ monitoring_module = ModuleDefinition( ], FrontendType.VENDOR: [], # No vendor menu items }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="platformHealth", + label_key="monitoring.menu.platform_health", + icon="chart-bar", + order=75, + is_super_admin_only=True, + items=[ + MenuItemDefinition( + id="platform-health", + label_key="monitoring.menu.capacity_monitor", + icon="chart-bar", + route="/admin/platform-health", + order=10, + ), + MenuItemDefinition( + id="testing", + label_key="monitoring.menu.testing_hub", + icon="beaker", + route="/admin/testing", + order=20, + ), + MenuItemDefinition( + id="code-quality", + label_key="monitoring.menu.code_quality", + icon="shield-check", + route="/admin/code-quality", + order=30, + ), + ], + ), + MenuSectionDefinition( + id="monitoring", + label_key="monitoring.menu.platform_monitoring", + icon="collection", + order=80, + is_super_admin_only=True, + items=[ + MenuItemDefinition( + id="imports", + label_key="monitoring.menu.import_jobs", + icon="cube", + route="/admin/imports", + order=10, + ), + MenuItemDefinition( + id="background-tasks", + label_key="monitoring.menu.background_tasks", + icon="collection", + route="/admin/background-tasks", + order=20, + ), + MenuItemDefinition( + id="logs", + label_key="monitoring.menu.application_logs", + icon="document-text", + route="/admin/logs", + order=30, + ), + ], + ), + ], + }, is_core=False, is_internal=True, # Internal module - admin-only, not customer-facing # ========================================================================= diff --git a/app/modules/monitoring/models/__init__.py b/app/modules/monitoring/models/__init__.py index e7618e71..ac092cf7 100644 --- a/app/modules/monitoring/models/__init__.py +++ b/app/modules/monitoring/models/__init__.py @@ -10,7 +10,7 @@ from app.modules.billing.models import CapacitySnapshot # Admin notification and logging models from app.modules.messaging.models import AdminNotification -from models.database.admin import PlatformAlert +from app.modules.tenancy.models import PlatformAlert __all__ = [ "CapacitySnapshot", diff --git a/app/modules/monitoring/routes/api/admin_audit.py b/app/modules/monitoring/routes/api/admin_audit.py index 47933032..d43d175c 100644 --- a/app/modules/monitoring/routes/api/admin_audit.py +++ b/app/modules/monitoring/routes/api/admin_audit.py @@ -18,7 +18,7 @@ from app.api.deps import get_current_admin_api from app.core.database import get_db from app.modules.monitoring.services.admin_audit_service import admin_audit_service from models.schema.auth import UserContext -from models.schema.admin import ( +from app.modules.tenancy.schemas.admin import ( AdminAuditLogFilters, AdminAuditLogListResponse, AdminAuditLogResponse, diff --git a/app/modules/monitoring/routes/api/admin_logs.py b/app/modules/monitoring/routes/api/admin_logs.py index 0abccdf0..e0c7f831 100644 --- a/app/modules/monitoring/routes/api/admin_logs.py +++ b/app/modules/monitoring/routes/api/admin_logs.py @@ -24,7 +24,7 @@ from app.modules.monitoring.services.admin_audit_service import admin_audit_serv from app.modules.core.services.admin_settings_service import admin_settings_service from app.modules.monitoring.services.log_service import log_service from models.schema.auth import UserContext -from models.schema.admin import ( +from app.modules.tenancy.schemas.admin import ( ApplicationLogFilters, ApplicationLogListResponse, FileLogResponse, @@ -280,7 +280,7 @@ def update_log_settings( Changes are applied immediately without restart (for log level). File rotation settings require restart. """ - from models.schema.admin import AdminSettingUpdate + from app.modules.tenancy.schemas.admin import AdminSettingUpdate updated = [] diff --git a/app/modules/monitoring/routes/pages/admin.py b/app/modules/monitoring/routes/pages/admin.py index 427a91fb..6f581df5 100644 --- a/app/modules/monitoring/routes/pages/admin.py +++ b/app/modules/monitoring/routes/pages/admin.py @@ -14,8 +14,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/monitoring/services/admin_audit_service.py b/app/modules/monitoring/services/admin_audit_service.py index 0e467727..7e5438f7 100644 --- a/app/modules/monitoring/services/admin_audit_service.py +++ b/app/modules/monitoring/services/admin_audit_service.py @@ -15,9 +15,9 @@ from sqlalchemy import and_ from sqlalchemy.orm import Session from app.modules.tenancy.exceptions import AdminOperationException -from models.database.admin import AdminAuditLog -from models.database.user import User -from models.schema.admin import AdminAuditLogFilters, AdminAuditLogResponse +from app.modules.tenancy.models import AdminAuditLog +from app.modules.tenancy.models import User +from app.modules.tenancy.schemas.admin import AdminAuditLogFilters, AdminAuditLogResponse logger = logging.getLogger(__name__) diff --git a/app/modules/monitoring/services/log_service.py b/app/modules/monitoring/services/log_service.py index 7bc0f3f7..8d4db9d4 100644 --- a/app/modules/monitoring/services/log_service.py +++ b/app/modules/monitoring/services/log_service.py @@ -20,8 +20,8 @@ from sqlalchemy.orm import Session from app.core.config import settings from app.exceptions import ResourceNotFoundException from app.modules.tenancy.exceptions import AdminOperationException -from models.database.admin import ApplicationLog -from models.schema.admin import ( +from app.modules.tenancy.models import ApplicationLog +from app.modules.tenancy.schemas.admin import ( ApplicationLogFilters, ApplicationLogListResponse, ApplicationLogResponse, diff --git a/app/modules/monitoring/services/platform_health_service.py b/app/modules/monitoring/services/platform_health_service.py index 584454d5..a3d14659 100644 --- a/app/modules/monitoring/services/platform_health_service.py +++ b/app/modules/monitoring/services/platform_health_service.py @@ -20,7 +20,7 @@ from app.modules.core.services.image_service import image_service from app.modules.inventory.models import Inventory from app.modules.orders.models import Order from app.modules.catalog.models import Product -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) @@ -173,7 +173,7 @@ class PlatformHealthService: Returns aggregated limits and current usage for capacity planning. """ from app.modules.billing.models import VendorSubscription - from models.database.vendor import VendorUser + from app.modules.tenancy.models import VendorUser # Get all active subscriptions with their limits subscriptions = ( diff --git a/app/modules/orders/definition.py b/app/modules/orders/definition.py index c6e02271..a61d1bd6 100644 --- a/app/modules/orders/definition.py +++ b/app/modules/orders/definition.py @@ -6,8 +6,8 @@ Defines the orders 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(): @@ -54,6 +54,44 @@ orders_module = ModuleDefinition( "orders", # Vendor order management ], }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="vendorOps", + label_key="orders.menu.vendor_operations", + icon="clipboard-list", + order=40, + items=[ + MenuItemDefinition( + id="orders", + label_key="orders.menu.orders", + icon="clipboard-list", + route="/admin/orders", + order=40, + ), + ], + ), + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="sales", + label_key="orders.menu.sales_orders", + icon="document-text", + order=20, + items=[ + MenuItemDefinition( + id="orders", + label_key="orders.menu.orders", + icon="document-text", + route="/vendor/{vendor_code}/orders", + order=10, + is_mandatory=True, + ), + ], + ), + ], + }, is_core=False, # ========================================================================= # Self-Contained Module Configuration diff --git a/app/modules/orders/locales/de.json b/app/modules/orders/locales/de.json index 0967ef42..d3be9d67 100644 --- a/app/modules/orders/locales/de.json +++ b/app/modules/orders/locales/de.json @@ -1 +1,50 @@ -{} +{ + "orders": { + "title": "Bestellungen", + "order": "Bestellung", + "order_id": "Bestellnummer", + "order_number": "Bestellnummer", + "order_date": "Bestelldatum", + "order_status": "Bestellstatus", + "order_details": "Bestelldetails", + "order_items": "Bestellartikel", + "order_total": "Bestellsumme", + "subtotal": "Zwischensumme", + "shipping": "Versand", + "tax": "Steuer", + "discount": "Rabatt", + "customer": "Kunde", + "shipping_address": "Lieferadresse", + "billing_address": "Rechnungsadresse", + "payment_method": "Zahlungsmethode", + "payment_status": "Zahlungsstatus", + "tracking": "Sendungsverfolgung", + "tracking_number": "Sendungsnummer", + "carrier": "Versanddienstleister", + "no_orders": "Keine Bestellungen gefunden", + "search_orders": "Bestellungen suchen...", + "filter_by_status": "Nach Status filtern", + "status_pending": "Ausstehend", + "status_processing": "In Bearbeitung", + "status_shipped": "Versendet", + "status_delivered": "Zugestellt", + "status_cancelled": "Storniert", + "status_refunded": "Erstattet", + "status_confirmed": "Bestätigt", + "status_rejected": "Abgelehnt", + "confirm_order": "Bestellung bestätigen", + "reject_order": "Bestellung ablehnen", + "set_tracking": "Sendungsverfolgung setzen", + "view_details": "Details ansehen" + }, + "messages": { + "order_status_updated": "Order status updated", + "item_shipped_successfully": "Item shipped successfully", + "all_items_shipped": "All items shipped", + "invoice_downloaded": "Invoice downloaded", + "failed_to_load_order_details": "Failed to load order details.", + "order_status_updated_successfully": "Order status updated successfully.", + "order_marked_as_shipped_successfully": "Order marked as shipped successfully.", + "no_shipping_label_url_available_for_this": "No shipping label URL available for this order." + } +} diff --git a/app/modules/orders/locales/en.json b/app/modules/orders/locales/en.json index 0967ef42..2f8dbd51 100644 --- a/app/modules/orders/locales/en.json +++ b/app/modules/orders/locales/en.json @@ -1 +1,50 @@ -{} +{ + "orders": { + "title": "Orders", + "order": "Order", + "order_id": "Order ID", + "order_number": "Order Number", + "order_date": "Order Date", + "order_status": "Order Status", + "order_details": "Order Details", + "order_items": "Order Items", + "order_total": "Order Total", + "subtotal": "Subtotal", + "shipping": "Shipping", + "tax": "Tax", + "discount": "Discount", + "customer": "Customer", + "shipping_address": "Shipping Address", + "billing_address": "Billing Address", + "payment_method": "Payment Method", + "payment_status": "Payment Status", + "tracking": "Tracking", + "tracking_number": "Tracking Number", + "carrier": "Carrier", + "no_orders": "No orders found", + "search_orders": "Search orders...", + "filter_by_status": "Filter by status", + "status_pending": "Pending", + "status_processing": "Processing", + "status_shipped": "Shipped", + "status_delivered": "Delivered", + "status_cancelled": "Cancelled", + "status_refunded": "Refunded", + "status_confirmed": "Confirmed", + "status_rejected": "Rejected", + "confirm_order": "Confirm Order", + "reject_order": "Reject Order", + "set_tracking": "Set Tracking", + "view_details": "View Details" + }, + "messages": { + "order_status_updated": "Order status updated", + "item_shipped_successfully": "Item shipped successfully", + "all_items_shipped": "All items shipped", + "invoice_downloaded": "Invoice downloaded", + "failed_to_load_order_details": "Failed to load order details.", + "order_status_updated_successfully": "Order status updated successfully.", + "order_marked_as_shipped_successfully": "Order marked as shipped successfully.", + "no_shipping_label_url_available_for_this": "No shipping label URL available for this order." + } +} diff --git a/app/modules/orders/locales/fr.json b/app/modules/orders/locales/fr.json index 0967ef42..e23126bf 100644 --- a/app/modules/orders/locales/fr.json +++ b/app/modules/orders/locales/fr.json @@ -1 +1,50 @@ -{} +{ + "orders": { + "title": "Commandes", + "order": "Commande", + "order_id": "ID de commande", + "order_number": "Numéro de commande", + "order_date": "Date de commande", + "order_status": "Statut de la commande", + "order_details": "Détails de la commande", + "order_items": "Articles de la commande", + "order_total": "Total de la commande", + "subtotal": "Sous-total", + "shipping": "Livraison", + "tax": "Taxe", + "discount": "Remise", + "customer": "Client", + "shipping_address": "Adresse de livraison", + "billing_address": "Adresse de facturation", + "payment_method": "Mode de paiement", + "payment_status": "Statut du paiement", + "tracking": "Suivi", + "tracking_number": "Numéro de suivi", + "carrier": "Transporteur", + "no_orders": "Aucune commande trouvée", + "search_orders": "Rechercher des commandes...", + "filter_by_status": "Filtrer par statut", + "status_pending": "En attente", + "status_processing": "En cours", + "status_shipped": "Expédiée", + "status_delivered": "Livrée", + "status_cancelled": "Annulée", + "status_refunded": "Remboursée", + "status_confirmed": "Confirmée", + "status_rejected": "Rejetée", + "confirm_order": "Confirmer la commande", + "reject_order": "Rejeter la commande", + "set_tracking": "Définir le suivi", + "view_details": "Voir les détails" + }, + "messages": { + "order_status_updated": "Order status updated", + "item_shipped_successfully": "Item shipped successfully", + "all_items_shipped": "All items shipped", + "invoice_downloaded": "Invoice downloaded", + "failed_to_load_order_details": "Failed to load order details.", + "order_status_updated_successfully": "Order status updated successfully.", + "order_marked_as_shipped_successfully": "Order marked as shipped successfully.", + "no_shipping_label_url_available_for_this": "No shipping label URL available for this order." + } +} diff --git a/app/modules/orders/locales/lb.json b/app/modules/orders/locales/lb.json index 0967ef42..45369385 100644 --- a/app/modules/orders/locales/lb.json +++ b/app/modules/orders/locales/lb.json @@ -1 +1,50 @@ -{} +{ + "orders": { + "title": "Bestellungen", + "order": "Bestellung", + "order_id": "Bestellungs-ID", + "order_number": "Bestellungsnummer", + "order_date": "Bestellungsdatum", + "order_status": "Bestellungsstatus", + "order_details": "Bestellungsdetailer", + "order_items": "Bestellungsartikelen", + "order_total": "Bestellungstotal", + "subtotal": "Subtotal", + "shipping": "Versand", + "tax": "Steier", + "discount": "Rabatt", + "customer": "Client", + "shipping_address": "Liwweradress", + "billing_address": "Rechnungsadress", + "payment_method": "Bezuelmethod", + "payment_status": "Bezuelstatus", + "tracking": "Tracking", + "tracking_number": "Trackingnummer", + "carrier": "Transporteur", + "no_orders": "Keng Bestellunge fonnt", + "search_orders": "Bestellunge sichen...", + "filter_by_status": "No Status filteren", + "status_pending": "Aussteesend", + "status_processing": "A Veraarbechtung", + "status_shipped": "Verschéckt", + "status_delivered": "Geliwwert", + "status_cancelled": "Annuléiert", + "status_refunded": "Rembourséiert", + "status_confirmed": "Bestätegt", + "status_rejected": "Ofgeleent", + "confirm_order": "Bestellung bestätegen", + "reject_order": "Bestellung oflehnen", + "set_tracking": "Tracking setzen", + "view_details": "Detailer kucken" + }, + "messages": { + "order_status_updated": "Order status updated", + "item_shipped_successfully": "Item shipped successfully", + "all_items_shipped": "All items shipped", + "invoice_downloaded": "Invoice downloaded", + "failed_to_load_order_details": "Failed to load order details.", + "order_status_updated_successfully": "Order status updated successfully.", + "order_marked_as_shipped_successfully": "Order marked as shipped successfully.", + "no_shipping_label_url_available_for_this": "No shipping label URL available for this order." + } +} diff --git a/app/modules/orders/routes/pages/admin.py b/app/modules/orders/routes/pages/admin.py index 51c7846f..69bfcb6d 100644 --- a/app/modules/orders/routes/pages/admin.py +++ b/app/modules/orders/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/orders/routes/pages/vendor.py b/app/modules/orders/routes/pages/vendor.py index f06eecb1..91d8464a 100644 --- a/app/modules/orders/routes/pages/vendor.py +++ b/app/modules/orders/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/orders/services/invoice_service.py b/app/modules/orders/services/invoice_service.py index 43bccdc9..9f92a9ac 100644 --- a/app/modules/orders/services/invoice_service.py +++ b/app/modules/orders/services/invoice_service.py @@ -35,7 +35,7 @@ from app.modules.orders.schemas.invoice import ( VendorInvoiceSettingsCreate, VendorInvoiceSettingsUpdate, ) -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor logger = logging.getLogger(__name__) diff --git a/app/modules/orders/services/order_service.py b/app/modules/orders/services/order_service.py index 8a87cbc5..e38ce0bc 100644 --- a/app/modules/orders/services/order_service.py +++ b/app/modules/orders/services/order_service.py @@ -49,7 +49,7 @@ from app.utils.vat import ( ) from app.modules.marketplace.models import MarketplaceProduct, MarketplaceProductTranslation from app.modules.catalog.models import Product -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor # Placeholder product constants PLACEHOLDER_GTIN = "0000000000000" diff --git a/app/modules/orders/static/admin/js/orders.js b/app/modules/orders/static/admin/js/orders.js index 7db416c7..a1252aee 100644 --- a/app/modules/orders/static/admin/js/orders.js +++ b/app/modules/orders/static/admin/js/orders.js @@ -135,6 +135,9 @@ function adminOrders() { }, async init() { + // Load i18n translations + await I18n.loadModule('orders'); + adminOrdersLog.info('Orders init() called'); // Guard against multiple initialization @@ -392,7 +395,7 @@ function adminOrders() { this.showDetailModal = true; } catch (error) { adminOrdersLog.error('Failed to load order details:', error); - Utils.showToast('Failed to load order details.', 'error'); + Utils.showToast(I18n.t('orders.messages.failed_to_load_order_details'), 'error'); } }, @@ -436,7 +439,7 @@ function adminOrders() { this.showStatusModal = false; this.selectedOrder = null; - Utils.showToast('Order status updated successfully.', 'success'); + Utils.showToast(I18n.t('orders.messages.order_status_updated_successfully'), 'success'); await this.refresh(); } catch (error) { @@ -487,7 +490,7 @@ function adminOrders() { this.showMarkAsShippedModal = false; this.selectedOrder = null; - Utils.showToast('Order marked as shipped successfully.', 'success'); + Utils.showToast(I18n.t('orders.messages.order_marked_as_shipped_successfully'), 'success'); await this.refresh(); } catch (error) { @@ -509,7 +512,7 @@ function adminOrders() { // Open label URL in new tab window.open(labelInfo.label_url, '_blank'); } else { - Utils.showToast('No shipping label URL available for this order.', 'warning'); + Utils.showToast(I18n.t('orders.messages.no_shipping_label_url_available_for_this'), 'warning'); } } catch (error) { adminOrdersLog.error('Failed to get shipping label:', error); diff --git a/app/modules/orders/static/vendor/js/order-detail.js b/app/modules/orders/static/vendor/js/order-detail.js index 7a07f574..0cfa1ab2 100644 --- a/app/modules/orders/static/vendor/js/order-detail.js +++ b/app/modules/orders/static/vendor/js/order-detail.js @@ -53,6 +53,9 @@ function vendorOrderDetail() { ], async init() { + // Load i18n translations + await I18n.loadModule('orders'); + orderDetailLog.info('Order detail init() called, orderId:', this.orderId); // Guard against multiple initialization @@ -262,7 +265,7 @@ function vendorOrderDetail() { {} ); - Utils.showToast('Item shipped successfully', 'success'); + Utils.showToast(I18n.t('orders.messages.item_shipped_successfully'), 'success'); await this.loadOrderDetails(); } catch (error) { @@ -301,7 +304,7 @@ function vendorOrderDetail() { payload ); - Utils.showToast('All items shipped', 'success'); + Utils.showToast(I18n.t('orders.messages.all_items_shipped'), 'success'); this.showShipAllModal = false; this.trackingNumber = ''; this.trackingProvider = ''; @@ -368,7 +371,7 @@ function vendorOrderDetail() { window.URL.revokeObjectURL(url); a.remove(); - Utils.showToast('Invoice downloaded', 'success'); + Utils.showToast(I18n.t('orders.messages.invoice_downloaded'), 'success'); } catch (error) { orderDetailLog.error('Failed to download invoice PDF:', error); diff --git a/app/modules/orders/static/vendor/js/orders.js b/app/modules/orders/static/vendor/js/orders.js index 59b6f9f7..96cd80d2 100644 --- a/app/modules/orders/static/vendor/js/orders.js +++ b/app/modules/orders/static/vendor/js/orders.js @@ -128,6 +128,9 @@ function vendorOrders() { }, async init() { + // Load i18n translations + await I18n.loadModule('orders'); + vendorOrdersLog.info('Orders init() called'); // Guard against multiple initialization @@ -277,7 +280,7 @@ function vendorOrders() { status: this.newStatus }); - Utils.showToast('Order status updated', 'success'); + Utils.showToast(I18n.t('orders.messages.order_status_updated'), 'success'); vendorOrdersLog.info('Updated order status:', this.selectedOrder.id, this.newStatus); this.showStatusModal = false; diff --git a/app/modules/payments/definition.py b/app/modules/payments/definition.py index 80778d3f..73fd2e43 100644 --- a/app/modules/payments/definition.py +++ b/app/modules/payments/definition.py @@ -16,7 +16,7 @@ This separation allows: """ from app.modules.base import ModuleDefinition -from models.database.admin_menu_config import FrontendType +from app.modules.enums import FrontendType def _get_admin_router(): diff --git a/app/modules/registry.py b/app/modules/registry.py index 0d9ecb35..4bfad15c 100644 --- a/app/modules/registry.py +++ b/app/modules/registry.py @@ -38,7 +38,7 @@ from functools import lru_cache from app.modules.base import ModuleDefinition from app.modules.discovery import discover_modules, discover_modules_by_tier -from models.database.admin_menu_config import FrontendType +from app.modules.enums import FrontendType logger = logging.getLogger(__name__) diff --git a/app/modules/service.py b/app/modules/service.py index d0405ff0..c13c3b1f 100644 --- a/app/modules/service.py +++ b/app/modules/service.py @@ -25,9 +25,9 @@ from app.modules.registry import ( get_menu_item_module, get_module, ) -from models.database.admin_menu_config import FrontendType -from models.database.platform import Platform -from models.database.platform_module import PlatformModule +from app.modules.enums import FrontendType +from app.modules.tenancy.models import Platform +from app.modules.tenancy.models import PlatformModule logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/definition.py b/app/modules/tenancy/definition.py index d8acf7f2..1d6abcb0 100644 --- a/app/modules/tenancy/definition.py +++ b/app/modules/tenancy/definition.py @@ -6,8 +6,8 @@ Platform, company, vendor, and admin user management. Required for multi-tenant 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 tenancy_module = ModuleDefinition( code="tenancy", @@ -22,6 +22,7 @@ tenancy_module = ModuleDefinition( "vendor_management", "admin_user_management", ], + # Legacy menu_items menu_items={ FrontendType.ADMIN: [ "platforms", @@ -33,6 +34,84 @@ tenancy_module = ModuleDefinition( "team", ], }, + # New module-driven menu definitions + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="superAdmin", + label_key="tenancy.menu.super_admin", + icon="shield", + order=10, + is_super_admin_only=True, + items=[ + MenuItemDefinition( + id="admin-users", + label_key="tenancy.menu.admin_users", + icon="shield", + route="/admin/admin-users", + order=10, + is_mandatory=True, + ), + ], + ), + MenuSectionDefinition( + id="platformAdmin", + label_key="tenancy.menu.platform_admin", + icon="office-building", + order=20, + items=[ + MenuItemDefinition( + id="companies", + label_key="tenancy.menu.companies", + icon="office-building", + route="/admin/companies", + order=10, + is_mandatory=True, + ), + MenuItemDefinition( + id="vendors", + label_key="tenancy.menu.vendors", + icon="shopping-bag", + route="/admin/vendors", + order=20, + is_mandatory=True, + ), + ], + ), + MenuSectionDefinition( + id="contentMgmt", + label_key="tenancy.menu.content_management", + icon="globe-alt", + order=70, + items=[ + MenuItemDefinition( + id="platforms", + label_key="tenancy.menu.platforms", + icon="globe-alt", + route="/admin/platforms", + order=10, + ), + ], + ), + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="account", + label_key="tenancy.menu.account_settings", + icon="user-group", + order=900, + items=[ + MenuItemDefinition( + id="team", + label_key="tenancy.menu.team", + icon="user-group", + route="/vendor/{vendor_code}/team", + order=5, + ), + ], + ), + ], + }, services_path="app.modules.tenancy.services", models_path="app.modules.tenancy.models", schemas_path="app.modules.tenancy.schemas", diff --git a/app/modules/tenancy/locales/de.json b/app/modules/tenancy/locales/de.json new file mode 100644 index 00000000..3b4eff61 --- /dev/null +++ b/app/modules/tenancy/locales/de.json @@ -0,0 +1,81 @@ +{ + "team": { + "title": "Team", + "members": "Mitglieder", + "add_member": "Mitglied hinzufügen", + "invite_member": "Mitglied einladen", + "remove_member": "Mitglied entfernen", + "role": "Rolle", + "owner": "Inhaber", + "manager": "Manager", + "editor": "Bearbeiter", + "viewer": "Betrachter", + "permissions": "Berechtigungen", + "pending_invitations": "Ausstehende Einladungen", + "invitation_sent": "Einladung gesendet", + "invitation_accepted": "Einladung angenommen" + }, + "messages": { + "business_info_saved": "Business info saved", + "marketplace_settings_saved": "Marketplace settings saved", + "please_enter_a_url_first": "Please enter a URL first", + "could_not_validate_url_it_may_still_work": "Could not validate URL - it may still work", + "localization_settings_saved": "Localization settings saved", + "failed_to_load_email_settings": "Failed to load email settings", + "from_email_and_from_name_are_required": "From Email and From Name are required", + "email_settings_saved": "Email settings saved", + "please_enter_a_test_email_address": "Please enter a test email address", + "please_save_your_email_settings_first": "Please save your email settings first", + "test_email_sent_check_your_inbox": "Test email sent! Check your inbox.", + "please_fix_the_errors_before_saving": "Please fix the errors before saving", + "profile_updated_successfully": "Profile updated successfully", + "email_is_required": "Email is required", + "invitation_sent_successfully": "Invitation sent successfully", + "team_member_updated": "Team member updated", + "team_member_removed": "Team member removed", + "invalid_company_url": "Invalid company URL", + "failed_to_load_company_details": "Failed to load company details", + "company_deleted_successfully": "Company deleted successfully", + "company_details_refreshed": "Company details refreshed", + "invalid_admin_user_url": "Invalid admin user URL", + "failed_to_load_admin_user_details": "Failed to load admin user details", + "you_cannot_deactivate_your_own_account": "You cannot deactivate your own account", + "you_cannot_delete_your_own_account": "You cannot delete your own account", + "admin_user_deleted_successfully": "Admin user deleted successfully", + "admin_user_details_refreshed": "Admin user details refreshed", + "failed_to_initialize_page": "Failed to initialize page", + "failed_to_load_company": "Failed to load company", + "company_updated_successfully": "Company updated successfully", + "ownership_transferred_successfully": "Ownership transferred successfully", + "theme_saved_successfully": "Theme saved successfully", + "failed_to_apply_preset": "Failed to apply preset", + "theme_reset_to_default": "Theme reset to default", + "failed_to_reset_theme": "Failed to reset theme", + "failed_to_load_vendors": "Failed to load vendors", + "vendor_deleted_successfully": "Vendor deleted successfully", + "vendors_list_refreshed": "Vendors list refreshed", + "invalid_user_url": "Invalid user URL", + "failed_to_load_user_details": "Failed to load user details", + "user_deleted_successfully": "User deleted successfully", + "user_details_refreshed": "User details refreshed", + "invalid_vendor_url": "Invalid vendor URL", + "failed_to_load_vendor_details": "Failed to load vendor details", + "no_vendor_loaded": "No vendor loaded", + "subscription_created_successfully": "Subscription created successfully", + "vendor_details_refreshed": "Vendor details refreshed", + "failed_to_load_users": "Failed to load users", + "failed_to_delete_user": "Failed to delete user", + "failed_to_load_admin_users": "Failed to load admin users", + "failed_to_load_admin_user": "Failed to load admin user", + "you_cannot_demote_yourself_from_super_ad": "You cannot demote yourself from super admin", + "platform_assigned_successfully": "Platform assigned successfully", + "platform_admin_must_be_assigned_to_at_le": "Platform admin must be assigned to at least one platform", + "platform_removed_successfully": "Platform removed successfully", + "please_fix_the_errors_before_submitting": "Please fix the errors before submitting", + "failed_to_load_vendor": "Failed to load vendor", + "vendor_updated_successfully": "Vendor updated successfully", + "all_contact_fields_reset_to_company_defa": "All contact fields reset to company defaults", + "failed_to_load_user": "Failed to load user", + "user_updated_successfully": "User updated successfully" + } +} diff --git a/app/modules/tenancy/locales/fr.json b/app/modules/tenancy/locales/fr.json new file mode 100644 index 00000000..a06b11c3 --- /dev/null +++ b/app/modules/tenancy/locales/fr.json @@ -0,0 +1,81 @@ +{ + "team": { + "title": "Équipe", + "members": "Membres", + "add_member": "Ajouter un membre", + "invite_member": "Inviter un membre", + "remove_member": "Retirer un membre", + "role": "Rôle", + "owner": "Propriétaire", + "manager": "Gestionnaire", + "editor": "Éditeur", + "viewer": "Lecteur", + "permissions": "Permissions", + "pending_invitations": "Invitations en attente", + "invitation_sent": "Invitation envoyée", + "invitation_accepted": "Invitation acceptée" + }, + "messages": { + "business_info_saved": "Business info saved", + "marketplace_settings_saved": "Marketplace settings saved", + "please_enter_a_url_first": "Please enter a URL first", + "could_not_validate_url_it_may_still_work": "Could not validate URL - it may still work", + "localization_settings_saved": "Localization settings saved", + "failed_to_load_email_settings": "Failed to load email settings", + "from_email_and_from_name_are_required": "From Email and From Name are required", + "email_settings_saved": "Email settings saved", + "please_enter_a_test_email_address": "Please enter a test email address", + "please_save_your_email_settings_first": "Please save your email settings first", + "test_email_sent_check_your_inbox": "Test email sent! Check your inbox.", + "please_fix_the_errors_before_saving": "Please fix the errors before saving", + "profile_updated_successfully": "Profile updated successfully", + "email_is_required": "Email is required", + "invitation_sent_successfully": "Invitation sent successfully", + "team_member_updated": "Team member updated", + "team_member_removed": "Team member removed", + "invalid_company_url": "Invalid company URL", + "failed_to_load_company_details": "Failed to load company details", + "company_deleted_successfully": "Company deleted successfully", + "company_details_refreshed": "Company details refreshed", + "invalid_admin_user_url": "Invalid admin user URL", + "failed_to_load_admin_user_details": "Failed to load admin user details", + "you_cannot_deactivate_your_own_account": "You cannot deactivate your own account", + "you_cannot_delete_your_own_account": "You cannot delete your own account", + "admin_user_deleted_successfully": "Admin user deleted successfully", + "admin_user_details_refreshed": "Admin user details refreshed", + "failed_to_initialize_page": "Failed to initialize page", + "failed_to_load_company": "Failed to load company", + "company_updated_successfully": "Company updated successfully", + "ownership_transferred_successfully": "Ownership transferred successfully", + "theme_saved_successfully": "Theme saved successfully", + "failed_to_apply_preset": "Failed to apply preset", + "theme_reset_to_default": "Theme reset to default", + "failed_to_reset_theme": "Failed to reset theme", + "failed_to_load_vendors": "Failed to load vendors", + "vendor_deleted_successfully": "Vendor deleted successfully", + "vendors_list_refreshed": "Vendors list refreshed", + "invalid_user_url": "Invalid user URL", + "failed_to_load_user_details": "Failed to load user details", + "user_deleted_successfully": "User deleted successfully", + "user_details_refreshed": "User details refreshed", + "invalid_vendor_url": "Invalid vendor URL", + "failed_to_load_vendor_details": "Failed to load vendor details", + "no_vendor_loaded": "No vendor loaded", + "subscription_created_successfully": "Subscription created successfully", + "vendor_details_refreshed": "Vendor details refreshed", + "failed_to_load_users": "Failed to load users", + "failed_to_delete_user": "Failed to delete user", + "failed_to_load_admin_users": "Failed to load admin users", + "failed_to_load_admin_user": "Failed to load admin user", + "you_cannot_demote_yourself_from_super_ad": "You cannot demote yourself from super admin", + "platform_assigned_successfully": "Platform assigned successfully", + "platform_admin_must_be_assigned_to_at_le": "Platform admin must be assigned to at least one platform", + "platform_removed_successfully": "Platform removed successfully", + "please_fix_the_errors_before_submitting": "Please fix the errors before submitting", + "failed_to_load_vendor": "Failed to load vendor", + "vendor_updated_successfully": "Vendor updated successfully", + "all_contact_fields_reset_to_company_defa": "All contact fields reset to company defaults", + "failed_to_load_user": "Failed to load user", + "user_updated_successfully": "User updated successfully" + } +} diff --git a/app/modules/tenancy/locales/lb.json b/app/modules/tenancy/locales/lb.json new file mode 100644 index 00000000..dd6f3c67 --- /dev/null +++ b/app/modules/tenancy/locales/lb.json @@ -0,0 +1,81 @@ +{ + "team": { + "title": "Team", + "members": "Memberen", + "add_member": "Member derbäisetzen", + "invite_member": "Member invitéieren", + "remove_member": "Member ewechhuelen", + "role": "Roll", + "owner": "Proprietär", + "manager": "Manager", + "editor": "Editeur", + "viewer": "Betruechter", + "permissions": "Rechter", + "pending_invitations": "Aussteesend Invitatiounen", + "invitation_sent": "Invitatioun geschéckt", + "invitation_accepted": "Invitatioun ugeholl" + }, + "messages": { + "business_info_saved": "Business info saved", + "marketplace_settings_saved": "Marketplace settings saved", + "please_enter_a_url_first": "Please enter a URL first", + "could_not_validate_url_it_may_still_work": "Could not validate URL - it may still work", + "localization_settings_saved": "Localization settings saved", + "failed_to_load_email_settings": "Failed to load email settings", + "from_email_and_from_name_are_required": "From Email and From Name are required", + "email_settings_saved": "Email settings saved", + "please_enter_a_test_email_address": "Please enter a test email address", + "please_save_your_email_settings_first": "Please save your email settings first", + "test_email_sent_check_your_inbox": "Test email sent! Check your inbox.", + "please_fix_the_errors_before_saving": "Please fix the errors before saving", + "profile_updated_successfully": "Profile updated successfully", + "email_is_required": "Email is required", + "invitation_sent_successfully": "Invitation sent successfully", + "team_member_updated": "Team member updated", + "team_member_removed": "Team member removed", + "invalid_company_url": "Invalid company URL", + "failed_to_load_company_details": "Failed to load company details", + "company_deleted_successfully": "Company deleted successfully", + "company_details_refreshed": "Company details refreshed", + "invalid_admin_user_url": "Invalid admin user URL", + "failed_to_load_admin_user_details": "Failed to load admin user details", + "you_cannot_deactivate_your_own_account": "You cannot deactivate your own account", + "you_cannot_delete_your_own_account": "You cannot delete your own account", + "admin_user_deleted_successfully": "Admin user deleted successfully", + "admin_user_details_refreshed": "Admin user details refreshed", + "failed_to_initialize_page": "Failed to initialize page", + "failed_to_load_company": "Failed to load company", + "company_updated_successfully": "Company updated successfully", + "ownership_transferred_successfully": "Ownership transferred successfully", + "theme_saved_successfully": "Theme saved successfully", + "failed_to_apply_preset": "Failed to apply preset", + "theme_reset_to_default": "Theme reset to default", + "failed_to_reset_theme": "Failed to reset theme", + "failed_to_load_vendors": "Failed to load vendors", + "vendor_deleted_successfully": "Vendor deleted successfully", + "vendors_list_refreshed": "Vendors list refreshed", + "invalid_user_url": "Invalid user URL", + "failed_to_load_user_details": "Failed to load user details", + "user_deleted_successfully": "User deleted successfully", + "user_details_refreshed": "User details refreshed", + "invalid_vendor_url": "Invalid vendor URL", + "failed_to_load_vendor_details": "Failed to load vendor details", + "no_vendor_loaded": "No vendor loaded", + "subscription_created_successfully": "Subscription created successfully", + "vendor_details_refreshed": "Vendor details refreshed", + "failed_to_load_users": "Failed to load users", + "failed_to_delete_user": "Failed to delete user", + "failed_to_load_admin_users": "Failed to load admin users", + "failed_to_load_admin_user": "Failed to load admin user", + "you_cannot_demote_yourself_from_super_ad": "You cannot demote yourself from super admin", + "platform_assigned_successfully": "Platform assigned successfully", + "platform_admin_must_be_assigned_to_at_le": "Platform admin must be assigned to at least one platform", + "platform_removed_successfully": "Platform removed successfully", + "please_fix_the_errors_before_submitting": "Please fix the errors before submitting", + "failed_to_load_vendor": "Failed to load vendor", + "vendor_updated_successfully": "Vendor updated successfully", + "all_contact_fields_reset_to_company_defa": "All contact fields reset to company defaults", + "failed_to_load_user": "Failed to load user", + "user_updated_successfully": "User updated successfully" + } +} diff --git a/app/modules/tenancy/models/__init__.py b/app/modules/tenancy/models/__init__.py index ceb9ee8c..7d2675dd 100644 --- a/app/modules/tenancy/models/__init__.py +++ b/app/modules/tenancy/models/__init__.py @@ -2,13 +2,53 @@ """ Tenancy module database models. -Models for platform, company, vendor, and admin user management. -Currently models remain in models/database/ - this package is a placeholder -for future migration. +This is the canonical location for tenancy module models including: +- Platform, Company, Vendor, User management +- Admin platform assignments +- Vendor platform memberships +- Platform module configuration +- Vendor domains """ -# Models will be migrated here from models/database/ -# For now, import from legacy location if needed: -# from models.database.vendor import Vendor -# from models.database.company import Company -# from models.database.platform import Platform +from app.modules.tenancy.models.admin import ( + AdminAuditLog, + AdminSession, + AdminSetting, + ApplicationLog, + PlatformAlert, +) +from app.modules.tenancy.models.admin_platform import AdminPlatform +from app.modules.tenancy.models.company import Company +from app.modules.tenancy.models.platform import Platform +from app.modules.tenancy.models.platform_module import PlatformModule +from app.modules.tenancy.models.user import User, UserRole +from app.modules.tenancy.models.vendor import Role, Vendor, VendorUser, VendorUserType +from app.modules.tenancy.models.vendor_domain import VendorDomain +from app.modules.tenancy.models.vendor_platform import VendorPlatform + +__all__ = [ + # Admin models + "AdminAuditLog", + "AdminSession", + "AdminSetting", + "ApplicationLog", + "PlatformAlert", + # Admin-Platform junction + "AdminPlatform", + # Company + "Company", + # Platform + "Platform", + "PlatformModule", + # User + "User", + "UserRole", + # Vendor + "Vendor", + "VendorUser", + "VendorUserType", + "Role", + # Vendor configuration + "VendorDomain", + "VendorPlatform", +] diff --git a/models/database/admin.py b/app/modules/tenancy/models/admin.py similarity index 97% rename from models/database/admin.py rename to app/modules/tenancy/models/admin.py index 32f99d49..18a209fa 100644 --- a/models/database/admin.py +++ b/app/modules/tenancy/models/admin.py @@ -1,5 +1,4 @@ -# Admin-specific models -# models/database/admin.py +# app/modules/tenancy/models/admin.py """ Admin-specific database models. @@ -24,8 +23,7 @@ from sqlalchemy import ( from sqlalchemy.orm import relationship from app.core.database import Base - -from .base import TimestampMixin +from models.database.base import TimestampMixin class AdminAuditLog(Base, TimestampMixin): @@ -196,3 +194,12 @@ class ApplicationLog(Base, TimestampMixin): def __repr__(self): return f"" + + +__all__ = [ + "AdminAuditLog", + "AdminSetting", + "PlatformAlert", + "AdminSession", + "ApplicationLog", +] diff --git a/models/database/admin_platform.py b/app/modules/tenancy/models/admin_platform.py similarity index 98% rename from models/database/admin_platform.py rename to app/modules/tenancy/models/admin_platform.py index c208f166..77144a46 100644 --- a/models/database/admin_platform.py +++ b/app/modules/tenancy/models/admin_platform.py @@ -1,4 +1,4 @@ -# models/database/admin_platform.py +# app/modules/tenancy/models/admin_platform.py """ AdminPlatform junction table for many-to-many relationship between Admin Users and Platforms. @@ -159,3 +159,6 @@ class AdminPlatform(Base, TimestampMixin): f"platform_id={self.platform_id}, " f"is_active={self.is_active})>" ) + + +__all__ = ["AdminPlatform"] diff --git a/models/database/company.py b/app/modules/tenancy/models/company.py similarity index 98% rename from models/database/company.py rename to app/modules/tenancy/models/company.py index 1ff986b1..4528a3b1 100644 --- a/models/database/company.py +++ b/app/modules/tenancy/models/company.py @@ -1,4 +1,4 @@ -# models/database/company.py +# app/modules/tenancy/models/company.py """ Company model representing the business entity that owns one or more vendor brands. @@ -104,3 +104,6 @@ class Company(Base, TimestampMixin): if not self.vendors: return 0 return sum(1 for v in self.vendors if v.is_active) + + +__all__ = ["Company"] diff --git a/models/database/platform.py b/app/modules/tenancy/models/platform.py similarity index 99% rename from models/database/platform.py rename to app/modules/tenancy/models/platform.py index f3d9abb5..da102d7c 100644 --- a/models/database/platform.py +++ b/app/modules/tenancy/models/platform.py @@ -1,4 +1,4 @@ -# models/database/platform.py +# app/modules/tenancy/models/platform.py """ Platform model representing a business offering/product line. @@ -237,3 +237,6 @@ class Platform(Base, TimestampMixin): def __repr__(self) -> str: return f"" + + +__all__ = ["Platform"] diff --git a/models/database/platform_module.py b/app/modules/tenancy/models/platform_module.py similarity index 98% rename from models/database/platform_module.py rename to app/modules/tenancy/models/platform_module.py index 57ca4232..67a90e4f 100644 --- a/models/database/platform_module.py +++ b/app/modules/tenancy/models/platform_module.py @@ -1,4 +1,4 @@ -# models/database/platform_module.py +# app/modules/tenancy/models/platform_module.py """ PlatformModule model for tracking module enablement per platform. @@ -160,3 +160,6 @@ class PlatformModule(Base, TimestampMixin): def __repr__(self) -> str: status = "enabled" if self.is_enabled else "disabled" return f"" + + +__all__ = ["PlatformModule"] diff --git a/models/database/user.py b/app/modules/tenancy/models/user.py similarity index 98% rename from models/database/user.py rename to app/modules/tenancy/models/user.py index 8cbf3f65..56ea3782 100644 --- a/models/database/user.py +++ b/app/modules/tenancy/models/user.py @@ -1,4 +1,4 @@ -# models/database/user.py - IMPROVED VERSION +# app/modules/tenancy/models/user.py """ User model with authentication support. @@ -108,7 +108,7 @@ class User(Base, TimestampMixin): Check if user is the owner of a specific vendor. Ownership is determined via company ownership: - User owns Company → Company has Vendor → User owns Vendor + User owns Company -> Company has Vendor -> User owns Vendor """ for company in self.owned_companies: if any(v.id == vendor_id for v in company.vendors): @@ -197,3 +197,6 @@ class User(Base, TimestampMixin): if self.is_super_admin: return None # None means ALL platforms return [ap.platform_id for ap in self.admin_platforms if ap.is_active] + + +__all__ = ["User", "UserRole"] diff --git a/models/database/vendor.py b/app/modules/tenancy/models/vendor.py similarity index 99% rename from models/database/vendor.py rename to app/modules/tenancy/models/vendor.py index a4c1ab43..566a3241 100644 --- a/models/database/vendor.py +++ b/app/modules/tenancy/models/vendor.py @@ -1,4 +1,4 @@ -# models/database/vendor.py +# app/modules/tenancy/models/vendor.py """ Vendor model representing entities that sell products or services. @@ -121,7 +121,7 @@ class Vendor(Base, TimestampMixin): JSON, nullable=False, default=["fr", "de", "en"] ) # Array of enabled languages for storefront language selector - # Currency/number formatting locale (e.g., 'fr-LU' = "29,99 €", 'en-GB' = "€29.99") + # Currency/number formatting locale (e.g., 'fr-LU' = "29,99 EUR", 'en-GB' = "EUR29.99") # NULL means inherit from platform default (AdminSetting 'default_storefront_locale') storefront_locale = Column(String(10), nullable=True) @@ -566,3 +566,6 @@ class Role(Base, TimestampMixin): str: A string that includes the id and name of the Role instance. """ return f"" + + +__all__ = ["Vendor", "VendorUser", "VendorUserType", "Role"] diff --git a/models/database/vendor_domain.py b/app/modules/tenancy/models/vendor_domain.py similarity index 88% rename from models/database/vendor_domain.py rename to app/modules/tenancy/models/vendor_domain.py index 65a56248..1be49eb7 100644 --- a/models/database/vendor_domain.py +++ b/app/modules/tenancy/models/vendor_domain.py @@ -1,4 +1,4 @@ -# models/database/vendor_domain.py +# app/modules/tenancy/models/vendor_domain.py """ Vendor Domain Model - Maps custom domains to vendors """ @@ -24,9 +24,9 @@ class VendorDomain(Base, TimestampMixin): Maps custom domains to vendors for multi-domain routing. Examples: - - customdomain1.com → Vendor 1 - - shop.mybusiness.com → Vendor 2 - - www.customdomain1.com → Vendor 1 (www is stripped) + - customdomain1.com -> Vendor 1 + - shop.mybusiness.com -> Vendor 2 + - www.customdomain1.com -> Vendor 1 (www is stripped) """ __tablename__ = "vendor_domains" @@ -76,9 +76,9 @@ class VendorDomain(Base, TimestampMixin): Normalize domain for consistent storage. Examples: - - https://example.com → example.com - - www.example.com → example.com - - EXAMPLE.COM → example.com + - https://example.com -> example.com + - www.example.com -> example.com + - EXAMPLE.COM -> example.com """ # Remove protocol domain = domain.replace("https://", "").replace("http://", "") # noqa: SEC-034 @@ -94,3 +94,6 @@ class VendorDomain(Base, TimestampMixin): domain = domain.lower() return domain + + +__all__ = ["VendorDomain"] diff --git a/models/database/vendor_platform.py b/app/modules/tenancy/models/vendor_platform.py similarity index 98% rename from models/database/vendor_platform.py rename to app/modules/tenancy/models/vendor_platform.py index 6396a089..1af7be8e 100644 --- a/models/database/vendor_platform.py +++ b/app/modules/tenancy/models/vendor_platform.py @@ -1,4 +1,4 @@ -# models/database/vendor_platform.py +# app/modules/tenancy/models/vendor_platform.py """ VendorPlatform junction table for many-to-many relationship between Vendor and Platform. @@ -187,3 +187,6 @@ class VendorPlatform(Base, TimestampMixin): f"platform_id={self.platform_id}, " f"is_active={self.is_active})>" ) + + +__all__ = ["VendorPlatform"] diff --git a/app/modules/tenancy/routes/api/admin.py b/app/modules/tenancy/routes/api/admin.py index e7d61bea..f9c74377 100644 --- a/app/modules/tenancy/routes/api/admin.py +++ b/app/modules/tenancy/routes/api/admin.py @@ -10,6 +10,8 @@ Aggregates all admin tenancy routes: - /platforms/* - Platform management (super admin only) - /vendors/* - Vendor management - /vendor-domains/* - Vendor domain configuration +- /modules/* - Platform module management +- /module-config/* - Module configuration management The tenancy module owns identity and organizational hierarchy. """ @@ -23,6 +25,8 @@ from .admin_companies import admin_companies_router from .admin_platforms import admin_platforms_router from .admin_vendors import admin_vendors_router from .admin_vendor_domains import admin_vendor_domains_router +from .admin_modules import router as admin_modules_router +from .admin_module_config import router as admin_module_config_router admin_router = APIRouter() @@ -34,3 +38,5 @@ admin_router.include_router(admin_companies_router, tags=["admin-companies"]) admin_router.include_router(admin_platforms_router, tags=["admin-platforms"]) admin_router.include_router(admin_vendors_router, tags=["admin-vendors"]) admin_router.include_router(admin_vendor_domains_router, tags=["admin-vendor-domains"]) +admin_router.include_router(admin_modules_router, tags=["admin-modules"]) +admin_router.include_router(admin_module_config_router, tags=["admin-module-config"]) diff --git a/app/modules/tenancy/routes/api/admin_auth.py b/app/modules/tenancy/routes/api/admin_auth.py index 460056e8..d51161a7 100644 --- a/app/modules/tenancy/routes/api/admin_auth.py +++ b/app/modules/tenancy/routes/api/admin_auth.py @@ -21,7 +21,7 @@ from app.modules.tenancy.exceptions import InsufficientPermissionsException, Inv from app.modules.tenancy.services.admin_platform_service import admin_platform_service from app.modules.core.services.auth_service import auth_service from middleware.auth import AuthManager -from models.database.platform import Platform # noqa: API-007 - Admin needs to query platforms +from app.modules.tenancy.models import Platform # noqa: API-007 - Admin needs to query platforms from models.schema.auth import UserContext from models.schema.auth import LoginResponse, LogoutResponse, UserLogin, UserResponse diff --git a/app/modules/tenancy/routes/api/admin_companies.py b/app/modules/tenancy/routes/api/admin_companies.py index f5c03249..e40c1f42 100644 --- a/app/modules/tenancy/routes/api/admin_companies.py +++ b/app/modules/tenancy/routes/api/admin_companies.py @@ -14,7 +14,7 @@ from app.core.database import get_db from app.modules.tenancy.exceptions import CompanyHasVendorsException, ConfirmationRequiredException from app.modules.tenancy.services.company_service import company_service from models.schema.auth import UserContext -from models.schema.company import ( +from app.modules.tenancy.schemas.company import ( CompanyCreate, CompanyCreateResponse, CompanyDetailResponse, diff --git a/app/api/v1/admin/module_config.py b/app/modules/tenancy/routes/api/admin_module_config.py similarity index 99% rename from app/api/v1/admin/module_config.py rename to app/modules/tenancy/routes/api/admin_module_config.py index bdc1d643..3384de0e 100644 --- a/app/api/v1/admin/module_config.py +++ b/app/modules/tenancy/routes/api/admin_module_config.py @@ -1,4 +1,4 @@ -# app/api/v1/admin/module_config.py +# app/modules/tenancy/routes/api/admin_module_config.py """ Admin API endpoints for Module Configuration Management. diff --git a/app/api/v1/admin/modules.py b/app/modules/tenancy/routes/api/admin_modules.py similarity index 98% rename from app/api/v1/admin/modules.py rename to app/modules/tenancy/routes/api/admin_modules.py index 374c9581..c4ca55b8 100644 --- a/app/api/v1/admin/modules.py +++ b/app/modules/tenancy/routes/api/admin_modules.py @@ -1,4 +1,4 @@ -# app/api/v1/admin/modules.py +# app/modules/tenancy/routes/api/admin_modules.py """ Admin API endpoints for Platform Module Management. @@ -100,7 +100,7 @@ def _build_module_response( is_enabled: bool, ) -> ModuleResponse: """Build ModuleResponse from module code.""" - 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 module = MODULES.get(code) if not module: diff --git a/app/modules/tenancy/routes/api/admin_users.py b/app/modules/tenancy/routes/api/admin_users.py index 56b701b7..9436e00f 100644 --- a/app/modules/tenancy/routes/api/admin_users.py +++ b/app/modules/tenancy/routes/api/admin_users.py @@ -23,7 +23,7 @@ from app.api.deps import get_current_super_admin, get_current_super_admin_api from app.core.database import get_db from app.exceptions import ValidationException from app.modules.tenancy.services.admin_platform_service import admin_platform_service -from models.database.user import User # noqa: API-007 - Internal helper uses User model +from app.modules.tenancy.models import User # noqa: API-007 - Internal helper uses User model from models.schema.auth import UserContext admin_users_router = APIRouter(prefix="/admin-users") diff --git a/app/modules/tenancy/routes/api/admin_vendor_domains.py b/app/modules/tenancy/routes/api/admin_vendor_domains.py index fed5515b..4c2221cb 100644 --- a/app/modules/tenancy/routes/api/admin_vendor_domains.py +++ b/app/modules/tenancy/routes/api/admin_vendor_domains.py @@ -19,7 +19,7 @@ from app.core.database import get_db from app.modules.tenancy.services.vendor_domain_service import vendor_domain_service from app.modules.tenancy.services.vendor_service import vendor_service from models.schema.auth import UserContext -from models.schema.vendor_domain import ( +from app.modules.tenancy.schemas.vendor_domain import ( DomainDeletionResponse, DomainVerificationInstructions, DomainVerificationResponse, diff --git a/app/modules/tenancy/routes/api/admin_vendors.py b/app/modules/tenancy/routes/api/admin_vendors.py index 78920f3f..556cace1 100644 --- a/app/modules/tenancy/routes/api/admin_vendors.py +++ b/app/modules/tenancy/routes/api/admin_vendors.py @@ -21,7 +21,7 @@ from app.modules.analytics.services.stats_service import stats_service from app.modules.tenancy.services.vendor_service import vendor_service from models.schema.auth import UserContext from app.modules.analytics.schemas import VendorStatsResponse -from models.schema.vendor import ( +from app.modules.tenancy.schemas.vendor import ( LetzshopExportRequest, LetzshopExportResponse, VendorCreate, diff --git a/app/modules/tenancy/routes/api/vendor.py b/app/modules/tenancy/routes/api/vendor.py index 87876531..01f94775 100644 --- a/app/modules/tenancy/routes/api/vendor.py +++ b/app/modules/tenancy/routes/api/vendor.py @@ -18,7 +18,7 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.modules.tenancy.services.vendor_service import vendor_service # noqa: mod-004 -from models.schema.vendor import VendorDetailResponse +from app.modules.tenancy.schemas.vendor import VendorDetailResponse vendor_router = APIRouter() logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/routes/api/vendor_profile.py b/app/modules/tenancy/routes/api/vendor_profile.py index 142113a7..843af3b5 100644 --- a/app/modules/tenancy/routes/api/vendor_profile.py +++ b/app/modules/tenancy/routes/api/vendor_profile.py @@ -15,7 +15,7 @@ from app.api.deps import get_current_vendor_api from app.core.database import get_db from app.modules.tenancy.services.vendor_service import vendor_service from models.schema.auth import UserContext -from models.schema.vendor import VendorResponse, VendorUpdate +from app.modules.tenancy.schemas.vendor import VendorResponse, VendorUpdate vendor_profile_router = APIRouter(prefix="/profile") logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/routes/api/vendor_team.py b/app/modules/tenancy/routes/api/vendor_team.py index 456d6085..4d641de3 100644 --- a/app/modules/tenancy/routes/api/vendor_team.py +++ b/app/modules/tenancy/routes/api/vendor_team.py @@ -25,7 +25,7 @@ from app.core.database import get_db from app.core.permissions import VendorPermissions from app.modules.tenancy.services.vendor_team_service import vendor_team_service from models.schema.auth import UserContext -from models.schema.team import ( +from app.modules.tenancy.schemas.team import ( BulkRemoveRequest, BulkRemoveResponse, InvitationAccept, diff --git a/app/modules/tenancy/routes/pages/admin.py b/app/modules/tenancy/routes/pages/admin.py index e4fb74ef..e9dcae96 100644 --- a/app/modules/tenancy/routes/pages/admin.py +++ b/app/modules/tenancy/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/tenancy/routes/pages/vendor.py b/app/modules/tenancy/routes/pages/vendor.py index 348d14c0..4ba8279f 100644 --- a/app/modules/tenancy/routes/pages/vendor.py +++ b/app/modules/tenancy/routes/pages/vendor.py @@ -21,7 +21,7 @@ from app.api.deps import ( ) 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/tenancy/schemas/__init__.py b/app/modules/tenancy/schemas/__init__.py index 22cd79d2..76c9a39b 100644 --- a/app/modules/tenancy/schemas/__init__.py +++ b/app/modules/tenancy/schemas/__init__.py @@ -2,11 +2,207 @@ """ Tenancy module Pydantic schemas. -Request/response schemas for platform, company, vendor, and admin user management. -Currently schemas remain in models/schema/ - this package is a placeholder -for future migration. +Request/response schemas for platform, company, vendor, admin user, and team management. """ -# Schemas will be migrated here from models/schema/ -# For now, import from legacy location if needed: -# from models.schema.vendor import VendorDetailResponse +# Company schemas +from app.modules.tenancy.schemas.company import ( + CompanyBase, + CompanyCreate, + CompanyCreateResponse, + CompanyDetailResponse, + CompanyListResponse, + CompanyResponse, + CompanySummary, + CompanyTransferOwnership, + CompanyTransferOwnershipResponse, + CompanyUpdate, +) + +# Vendor schemas +from app.modules.tenancy.schemas.vendor import ( + LetzshopExportFileInfo, + LetzshopExportRequest, + LetzshopExportResponse, + VendorCreate, + VendorCreateResponse, + VendorDetailResponse, + VendorListResponse, + VendorResponse, + VendorSummary, + VendorUpdate, +) + +# Admin schemas +from app.modules.tenancy.schemas.admin import ( + AdminAuditLogFilters, + AdminAuditLogListResponse, + AdminAuditLogResponse, + AdminDashboardStats, + AdminNotificationCreate, + AdminNotificationListResponse, + AdminNotificationResponse, + AdminNotificationUpdate, + AdminSessionListResponse, + AdminSessionResponse, + AdminSettingCreate, + AdminSettingDefaultResponse, + AdminSettingListResponse, + AdminSettingResponse, + AdminSettingUpdate, + ApplicationLogFilters, + ApplicationLogListResponse, + ApplicationLogResponse, + BulkUserAction, + BulkUserActionResponse, + BulkVendorAction, + BulkVendorActionResponse, + ComponentHealthStatus, + FileLogResponse, + LogCleanupResponse, + LogDeleteResponse, + LogFileInfo, + LogFileListResponse, + LogSettingsResponse, + LogSettingsUpdate, + LogSettingsUpdateResponse, + LogStatistics, + PlatformAlertCreate, + PlatformAlertListResponse, + PlatformAlertResolve, + PlatformAlertResponse, + PublicDisplaySettingsResponse, + RowsPerPageResponse, + RowsPerPageUpdateResponse, + SystemHealthResponse, +) + +# Team schemas +from app.modules.tenancy.schemas.team import ( + BulkRemoveRequest, + BulkRemoveResponse, + InvitationAccept, + InvitationAcceptResponse, + InvitationResponse, + PermissionCheckRequest, + PermissionCheckResponse, + RoleBase, + RoleCreate, + RoleListResponse, + RoleResponse, + RoleUpdate, + TeamErrorResponse, + TeamMemberBase, + TeamMemberInvite, + TeamMemberListResponse, + TeamMemberResponse, + TeamMemberUpdate, + TeamStatistics, + UserPermissionsResponse, +) + +# Vendor domain schemas +from app.modules.tenancy.schemas.vendor_domain import ( + DomainDeletionResponse, + DomainVerificationInstructions, + DomainVerificationResponse, + VendorDomainCreate, + VendorDomainListResponse, + VendorDomainResponse, + VendorDomainUpdate, +) + +__all__ = [ + # Company + "CompanyBase", + "CompanyCreate", + "CompanyCreateResponse", + "CompanyDetailResponse", + "CompanyListResponse", + "CompanyResponse", + "CompanySummary", + "CompanyTransferOwnership", + "CompanyTransferOwnershipResponse", + "CompanyUpdate", + # Vendor + "LetzshopExportFileInfo", + "LetzshopExportRequest", + "LetzshopExportResponse", + "VendorCreate", + "VendorCreateResponse", + "VendorDetailResponse", + "VendorListResponse", + "VendorResponse", + "VendorSummary", + "VendorUpdate", + # Admin + "AdminAuditLogFilters", + "AdminAuditLogListResponse", + "AdminAuditLogResponse", + "AdminDashboardStats", + "AdminNotificationCreate", + "AdminNotificationListResponse", + "AdminNotificationResponse", + "AdminNotificationUpdate", + "AdminSessionListResponse", + "AdminSessionResponse", + "AdminSettingCreate", + "AdminSettingDefaultResponse", + "AdminSettingListResponse", + "AdminSettingResponse", + "AdminSettingUpdate", + "ApplicationLogFilters", + "ApplicationLogListResponse", + "ApplicationLogResponse", + "BulkUserAction", + "BulkUserActionResponse", + "BulkVendorAction", + "BulkVendorActionResponse", + "ComponentHealthStatus", + "FileLogResponse", + "LogCleanupResponse", + "LogDeleteResponse", + "LogFileInfo", + "LogFileListResponse", + "LogSettingsResponse", + "LogSettingsUpdate", + "LogSettingsUpdateResponse", + "LogStatistics", + "PlatformAlertCreate", + "PlatformAlertListResponse", + "PlatformAlertResolve", + "PlatformAlertResponse", + "PublicDisplaySettingsResponse", + "RowsPerPageResponse", + "RowsPerPageUpdateResponse", + "SystemHealthResponse", + # Team + "BulkRemoveRequest", + "BulkRemoveResponse", + "InvitationAccept", + "InvitationAcceptResponse", + "InvitationResponse", + "PermissionCheckRequest", + "PermissionCheckResponse", + "RoleBase", + "RoleCreate", + "RoleListResponse", + "RoleResponse", + "RoleUpdate", + "TeamErrorResponse", + "TeamMemberBase", + "TeamMemberInvite", + "TeamMemberListResponse", + "TeamMemberResponse", + "TeamMemberUpdate", + "TeamStatistics", + "UserPermissionsResponse", + # Vendor Domain + "DomainDeletionResponse", + "DomainVerificationInstructions", + "DomainVerificationResponse", + "VendorDomainCreate", + "VendorDomainListResponse", + "VendorDomainResponse", + "VendorDomainUpdate", +] diff --git a/models/schema/admin.py b/app/modules/tenancy/schemas/admin.py similarity index 99% rename from models/schema/admin.py rename to app/modules/tenancy/schemas/admin.py index 38762e03..5dd964d6 100644 --- a/models/schema/admin.py +++ b/app/modules/tenancy/schemas/admin.py @@ -1,4 +1,4 @@ -# models/schema/admin.py +# app/modules/tenancy/schemas/admin.py """ Admin-specific Pydantic schemas for API validation and responses. diff --git a/models/schema/company.py b/app/modules/tenancy/schemas/company.py similarity index 99% rename from models/schema/company.py rename to app/modules/tenancy/schemas/company.py index b7d7b55d..071de10a 100644 --- a/models/schema/company.py +++ b/app/modules/tenancy/schemas/company.py @@ -1,4 +1,4 @@ -# models/schema/company.py +# app/modules/tenancy/schemas/company.py """ Pydantic schemas for Company model. diff --git a/models/schema/team.py b/app/modules/tenancy/schemas/team.py similarity index 99% rename from models/schema/team.py rename to app/modules/tenancy/schemas/team.py index eed771ce..1183c4d2 100644 --- a/models/schema/team.py +++ b/app/modules/tenancy/schemas/team.py @@ -1,4 +1,4 @@ -# models/schema/team.py +# app/modules/tenancy/schemas/team.py """ Pydantic schemas for vendor team management. diff --git a/models/schema/vendor.py b/app/modules/tenancy/schemas/vendor.py similarity index 99% rename from models/schema/vendor.py rename to app/modules/tenancy/schemas/vendor.py index 66fc1c86..2301530b 100644 --- a/models/schema/vendor.py +++ b/app/modules/tenancy/schemas/vendor.py @@ -1,4 +1,4 @@ -# models/schema/vendor.py +# app/modules/tenancy/schemas/vendor.py """ Pydantic schemas for Vendor-related operations. diff --git a/models/schema/vendor_domain.py b/app/modules/tenancy/schemas/vendor_domain.py similarity index 98% rename from models/schema/vendor_domain.py rename to app/modules/tenancy/schemas/vendor_domain.py index 610446c7..fa413ccb 100644 --- a/models/schema/vendor_domain.py +++ b/app/modules/tenancy/schemas/vendor_domain.py @@ -1,4 +1,4 @@ -# models/schema/vendor_domain.py +# app/modules/tenancy/schemas/vendor_domain.py """ Pydantic schemas for Vendor Domain operations. diff --git a/app/modules/tenancy/services/admin_platform_service.py b/app/modules/tenancy/services/admin_platform_service.py index 1c2a664e..8a6ca22b 100644 --- a/app/modules/tenancy/services/admin_platform_service.py +++ b/app/modules/tenancy/services/admin_platform_service.py @@ -20,9 +20,9 @@ from app.modules.tenancy.exceptions import ( AdminOperationException, CannotModifySelfException, ) -from models.database.admin_platform import AdminPlatform -from models.database.platform import Platform -from models.database.user import User +from app.modules.tenancy.models import AdminPlatform +from app.modules.tenancy.models import Platform +from app.modules.tenancy.models import User logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/services/admin_service.py b/app/modules/tenancy/services/admin_service.py index c1b8244a..9bdee121 100644 --- a/app/modules/tenancy/services/admin_service.py +++ b/app/modules/tenancy/services/admin_service.py @@ -32,13 +32,13 @@ from app.modules.tenancy.exceptions import ( VendorVerificationException, ) from middleware.auth import AuthManager -from models.database.company import Company +from app.modules.tenancy.models import Company from app.modules.marketplace.models import MarketplaceImportJob -from models.database.platform import Platform -from models.database.user import User -from models.database.vendor import Role, Vendor +from app.modules.tenancy.models import Platform +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Role, Vendor from app.modules.marketplace.schemas import MarketplaceImportJobResponse -from models.schema.vendor import VendorCreate +from app.modules.tenancy.schemas.vendor import VendorCreate logger = logging.getLogger(__name__) @@ -422,7 +422,7 @@ class AdminService: # Assign vendor to platforms if provided if vendor_data.platform_ids: - from models.database.vendor_platform import VendorPlatform + from app.modules.tenancy.models import VendorPlatform for platform_id in vendor_data.platform_ids: # Verify platform exists diff --git a/app/modules/tenancy/services/company_service.py b/app/modules/tenancy/services/company_service.py index 8b9d6aa7..83cb57b8 100644 --- a/app/modules/tenancy/services/company_service.py +++ b/app/modules/tenancy/services/company_service.py @@ -13,9 +13,9 @@ from sqlalchemy import func, select from sqlalchemy.orm import Session, joinedload from app.modules.tenancy.exceptions import CompanyNotFoundException, UserNotFoundException -from models.database.company import Company -from models.database.user import User -from models.schema.company import CompanyCreate, CompanyTransferOwnership, CompanyUpdate +from app.modules.tenancy.models import Company +from app.modules.tenancy.models import User +from app.modules.tenancy.schemas.company import CompanyCreate, CompanyTransferOwnership, CompanyUpdate logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/services/platform_service.py b/app/modules/tenancy/services/platform_service.py index 51412e80..b1db2a45 100644 --- a/app/modules/tenancy/services/platform_service.py +++ b/app/modules/tenancy/services/platform_service.py @@ -21,8 +21,8 @@ from app.modules.tenancy.exceptions import ( PlatformNotFoundException, ) from app.modules.cms.models import ContentPage -from models.database.platform import Platform -from models.database.vendor_platform import VendorPlatform +from app.modules.tenancy.models import Platform +from app.modules.tenancy.models import VendorPlatform logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/services/team_service.py b/app/modules/tenancy/services/team_service.py index c80e69fc..43471c00 100644 --- a/app/modules/tenancy/services/team_service.py +++ b/app/modules/tenancy/services/team_service.py @@ -15,8 +15,8 @@ from typing import Any from sqlalchemy.orm import Session from app.exceptions import ValidationException -from models.database.user import User -from models.database.vendor import Role, VendorUser +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Role, VendorUser logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/services/vendor_domain_service.py b/app/modules/tenancy/services/vendor_domain_service.py index 723f0612..bdca32d1 100644 --- a/app/modules/tenancy/services/vendor_domain_service.py +++ b/app/modules/tenancy/services/vendor_domain_service.py @@ -29,9 +29,9 @@ from app.modules.tenancy.exceptions import ( VendorDomainNotFoundException, VendorNotFoundException, ) -from models.database.vendor import Vendor -from models.database.vendor_domain import VendorDomain -from models.schema.vendor_domain import VendorDomainCreate, VendorDomainUpdate +from app.modules.tenancy.models import Vendor +from app.modules.tenancy.models import VendorDomain +from app.modules.tenancy.schemas.vendor_domain import VendorDomainCreate, VendorDomainUpdate logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/services/vendor_service.py b/app/modules/tenancy/services/vendor_service.py index 554e2995..f1bbfa25 100644 --- a/app/modules/tenancy/services/vendor_service.py +++ b/app/modules/tenancy/services/vendor_service.py @@ -25,10 +25,10 @@ from app.modules.tenancy.exceptions import ( ) from app.modules.marketplace.models import MarketplaceProduct 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 from app.modules.catalog.schemas import ProductCreate -from models.schema.vendor import VendorCreate +from app.modules.tenancy.schemas.vendor import VendorCreate logger = logging.getLogger(__name__) @@ -63,7 +63,7 @@ class VendorService: UnauthorizedVendorAccessException: If user is not company owner InvalidVendorDataException: If vendor data is invalid """ - from models.database.company import Company + from app.modules.tenancy.models import Company try: # Validate company_id is provided @@ -159,7 +159,7 @@ class VendorService: # Non-admin users can only see active and verified vendors, plus their own if current_user.role != "admin": # Get vendor IDs the user owns through companies - from models.database.company import Company + from app.modules.tenancy.models import Company owned_vendor_ids = ( db.query(Vendor.id) @@ -243,7 +243,7 @@ class VendorService: """ from sqlalchemy.orm import joinedload - from models.database.company import Company + from app.modules.tenancy.models import Company vendor = ( db.query(Vendor) @@ -291,7 +291,7 @@ class VendorService: """ from sqlalchemy.orm import joinedload - from models.database.company import Company + from app.modules.tenancy.models import Company vendor = ( db.query(Vendor) @@ -325,7 +325,7 @@ class VendorService: """ from sqlalchemy.orm import joinedload - from models.database.company import Company + from app.modules.tenancy.models import Company # Try as integer ID first try: diff --git a/app/modules/tenancy/services/vendor_team_service.py b/app/modules/tenancy/services/vendor_team_service.py index b869683c..b16a9623 100644 --- a/app/modules/tenancy/services/vendor_team_service.py +++ b/app/modules/tenancy/services/vendor_team_service.py @@ -26,8 +26,8 @@ from app.modules.tenancy.exceptions import ( ) from app.modules.billing.exceptions import TierLimitExceededException from middleware.auth import AuthManager -from models.database.user import User -from models.database.vendor import Role, Vendor, VendorUser, VendorUserType +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Role, Vendor, VendorUser, VendorUserType logger = logging.getLogger(__name__) diff --git a/app/modules/tenancy/static/admin/js/admin-user-detail.js b/app/modules/tenancy/static/admin/js/admin-user-detail.js index 97816741..e7aa6efc 100644 --- a/app/modules/tenancy/static/admin/js/admin-user-detail.js +++ b/app/modules/tenancy/static/admin/js/admin-user-detail.js @@ -20,6 +20,9 @@ function adminUserDetailPage() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + adminUserDetailLog.info('=== ADMIN USER DETAIL PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -43,7 +46,7 @@ function adminUserDetailPage() { } else { adminUserDetailLog.error('No user ID in URL'); this.error = 'Invalid admin user URL'; - Utils.showToast('Invalid admin user URL', 'error'); + Utils.showToast(I18n.t('tenancy.messages.invalid_admin_user_url'), 'error'); } adminUserDetailLog.info('=== ADMIN USER DETAIL PAGE INITIALIZATION COMPLETE ==='); @@ -88,7 +91,7 @@ function adminUserDetailPage() { } catch (error) { window.LogConfig.logError(error, 'Load Admin User Details'); this.error = error.message || 'Failed to load admin user details'; - Utils.showToast('Failed to load admin user details', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_admin_user_details'), 'error'); } finally { this.loading = false; } @@ -109,7 +112,7 @@ function adminUserDetailPage() { // Prevent self-deactivation if (this.adminUser.id === this.currentUserId) { - Utils.showToast('You cannot deactivate your own account', 'error'); + Utils.showToast(I18n.t('tenancy.messages.you_cannot_deactivate_your_own_account'), 'error'); return; } @@ -145,7 +148,7 @@ function adminUserDetailPage() { // Prevent self-deletion if (this.adminUser.id === this.currentUserId) { - Utils.showToast('You cannot delete your own account', 'error'); + Utils.showToast(I18n.t('tenancy.messages.you_cannot_delete_your_own_account'), 'error'); return; } @@ -169,7 +172,7 @@ function adminUserDetailPage() { window.LogConfig.logApiCall('DELETE', url, null, 'response'); - Utils.showToast('Admin user deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.admin_user_deleted_successfully'), 'success'); adminUserDetailLog.info('Admin user deleted successfully'); // Redirect to admin users list @@ -187,7 +190,7 @@ function adminUserDetailPage() { async refresh() { adminUserDetailLog.info('=== ADMIN USER REFRESH TRIGGERED ==='); await this.loadAdminUser(); - Utils.showToast('Admin user details refreshed', 'success'); + Utils.showToast(I18n.t('tenancy.messages.admin_user_details_refreshed'), 'success'); adminUserDetailLog.info('=== ADMIN USER REFRESH COMPLETE ==='); } }; diff --git a/app/modules/tenancy/static/admin/js/admin-user-edit.js b/app/modules/tenancy/static/admin/js/admin-user-edit.js index 2946138e..99a13c4f 100644 --- a/app/modules/tenancy/static/admin/js/admin-user-edit.js +++ b/app/modules/tenancy/static/admin/js/admin-user-edit.js @@ -29,6 +29,9 @@ function adminUserEditPage() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + adminUserEditLog.info('=== ADMIN USER EDIT PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -52,7 +55,7 @@ function adminUserEditPage() { await this.loadAllPlatforms(); } else { adminUserEditLog.error('No user ID in URL'); - Utils.showToast('Invalid admin user URL', 'error'); + Utils.showToast(I18n.t('tenancy.messages.invalid_admin_user_url'), 'error'); setTimeout(() => window.location.href = '/admin/admin-users', 2000); } @@ -94,7 +97,7 @@ function adminUserEditPage() { } catch (error) { window.LogConfig.logError(error, 'Load Admin User'); - Utils.showToast('Failed to load admin user', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_admin_user'), 'error'); setTimeout(() => window.location.href = '/admin/admin-users', 2000); } finally { this.loading = false; @@ -137,7 +140,7 @@ function adminUserEditPage() { // Prevent self-demotion if (this.adminUser.id === this.currentUserId && !newStatus) { - Utils.showToast('You cannot demote yourself from super admin', 'error'); + Utils.showToast(I18n.t('tenancy.messages.you_cannot_demote_yourself_from_super_ad'), 'error'); return; } @@ -180,7 +183,7 @@ function adminUserEditPage() { // Prevent self-deactivation if (this.adminUser.id === this.currentUserId) { - Utils.showToast('You cannot deactivate your own account', 'error'); + Utils.showToast(I18n.t('tenancy.messages.you_cannot_deactivate_your_own_account'), 'error'); return; } @@ -228,7 +231,7 @@ function adminUserEditPage() { // Reload admin user to get updated platforms await this.loadAdminUser(); - Utils.showToast('Platform assigned successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.platform_assigned_successfully'), 'success'); adminUserEditLog.info('Platform assigned successfully'); this.showPlatformModal = false; this.selectedPlatformId = null; @@ -245,7 +248,7 @@ function adminUserEditPage() { removePlatform(platformId) { // Validate: platform admin must have at least one platform if (this.adminUser.platforms.length <= 1) { - Utils.showToast('Platform admin must be assigned to at least one platform', 'error'); + Utils.showToast(I18n.t('tenancy.messages.platform_admin_must_be_assigned_to_at_le'), 'error'); return; } @@ -272,7 +275,7 @@ function adminUserEditPage() { // Reload admin user to get updated platforms await this.loadAdminUser(); - Utils.showToast('Platform removed successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.platform_removed_successfully'), 'success'); adminUserEditLog.info('Platform removed successfully'); } catch (error) { @@ -296,7 +299,7 @@ function adminUserEditPage() { // Prevent self-deletion if (this.adminUser.id === this.currentUserId) { - Utils.showToast('You cannot delete your own account', 'error'); + Utils.showToast(I18n.t('tenancy.messages.you_cannot_delete_your_own_account'), 'error'); return; } @@ -320,7 +323,7 @@ function adminUserEditPage() { window.LogConfig.logApiCall('DELETE', url, null, 'response'); - Utils.showToast('Admin user deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.admin_user_deleted_successfully'), 'success'); adminUserEditLog.info('Admin user deleted successfully'); // Redirect to admin users list diff --git a/app/modules/tenancy/static/admin/js/admin-users.js b/app/modules/tenancy/static/admin/js/admin-users.js index 2c32e1ea..c4375fbd 100644 --- a/app/modules/tenancy/static/admin/js/admin-users.js +++ b/app/modules/tenancy/static/admin/js/admin-users.js @@ -37,6 +37,9 @@ function adminUsersPage() { // Initialization async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + adminUsersLog.info('=== ADMIN USERS PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -197,7 +200,7 @@ function adminUsersPage() { } catch (error) { window.LogConfig.logError(error, 'Load Admin Users'); this.error = error.message || 'Failed to load admin users'; - Utils.showToast('Failed to load admin users', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_admin_users'), 'error'); } finally { this.loading = false; } @@ -288,7 +291,7 @@ function adminUsersPage() { // Prevent self-deletion if (admin.id === this.currentUserId) { - Utils.showToast('You cannot delete your own account', 'error'); + Utils.showToast(I18n.t('tenancy.messages.you_cannot_delete_your_own_account'), 'error'); return; } @@ -309,7 +312,7 @@ function adminUsersPage() { await apiClient.delete(url); - Utils.showToast('Admin user deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.admin_user_deleted_successfully'), 'success'); adminUsersLog.info('Admin user deleted successfully'); await this.loadAdminUsers(); diff --git a/app/modules/tenancy/static/admin/js/company-detail.js b/app/modules/tenancy/static/admin/js/company-detail.js index dd1b1428..5d6c1f77 100644 --- a/app/modules/tenancy/static/admin/js/company-detail.js +++ b/app/modules/tenancy/static/admin/js/company-detail.js @@ -18,6 +18,9 @@ function adminCompanyDetail() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + companyDetailLog.info('=== COMPANY DETAIL PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -38,7 +41,7 @@ function adminCompanyDetail() { } else { companyDetailLog.error('No company ID in URL'); this.error = 'Invalid company URL'; - Utils.showToast('Invalid company URL', 'error'); + Utils.showToast(I18n.t('tenancy.messages.invalid_company_url'), 'error'); } companyDetailLog.info('=== COMPANY DETAIL PAGE INITIALIZATION COMPLETE ==='); @@ -75,7 +78,7 @@ function adminCompanyDetail() { } catch (error) { window.LogConfig.logError(error, 'Load Company Details'); this.error = error.message || 'Failed to load company details'; - Utils.showToast('Failed to load company details', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_company_details'), 'error'); } finally { this.loading = false; } @@ -121,7 +124,7 @@ function adminCompanyDetail() { window.LogConfig.logApiCall('DELETE', url, null, 'response'); - Utils.showToast('Company deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.company_deleted_successfully'), 'success'); companyDetailLog.info('Company deleted successfully'); // Redirect to companies list @@ -137,7 +140,7 @@ function adminCompanyDetail() { async refresh() { companyDetailLog.info('=== COMPANY REFRESH TRIGGERED ==='); await this.loadCompany(); - Utils.showToast('Company details refreshed', 'success'); + Utils.showToast(I18n.t('tenancy.messages.company_details_refreshed'), 'success'); companyDetailLog.info('=== COMPANY REFRESH COMPLETE ==='); } }; diff --git a/app/modules/tenancy/static/admin/js/company-edit.js b/app/modules/tenancy/static/admin/js/company-edit.js index 75437983..1f03716a 100644 --- a/app/modules/tenancy/static/admin/js/company-edit.js +++ b/app/modules/tenancy/static/admin/js/company-edit.js @@ -39,6 +39,9 @@ function adminCompanyEdit() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + companyEditLog.info('=== COMPANY EDIT PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -59,14 +62,14 @@ function adminCompanyEdit() { await this.loadCompany(); } else { companyEditLog.error('No company ID in URL'); - Utils.showToast('Invalid company URL', 'error'); + Utils.showToast(I18n.t('tenancy.messages.invalid_company_url'), 'error'); setTimeout(() => window.location.href = '/admin/companies', 2000); } companyEditLog.info('=== COMPANY EDIT PAGE INITIALIZATION COMPLETE ==='); } catch (error) { window.LogConfig.logError(error, 'Company Edit Init'); - Utils.showToast('Failed to initialize page', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_initialize_page'), 'error'); } }, @@ -107,7 +110,7 @@ function adminCompanyEdit() { } catch (error) { window.LogConfig.logError(error, 'Load Company'); - Utils.showToast('Failed to load company', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_company'), 'error'); setTimeout(() => window.location.href = '/admin/companies', 2000); } finally { this.loadingCompany = false; @@ -134,7 +137,7 @@ function adminCompanyEdit() { window.LogConfig.logPerformance('Update Company', duration); this.company = response; - Utils.showToast('Company updated successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.company_updated_successfully'), 'success'); companyEditLog.info(`Company updated successfully in ${duration}ms`, response); } catch (error) { @@ -258,7 +261,7 @@ function adminCompanyEdit() { window.LogConfig.logApiCall('POST', url, response, 'response'); - Utils.showToast('Ownership transferred successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.ownership_transferred_successfully'), 'success'); companyEditLog.info('Ownership transferred successfully', response); // Close modal and reload company data @@ -370,7 +373,7 @@ function adminCompanyEdit() { window.LogConfig.logApiCall('DELETE', url, response, 'response'); - Utils.showToast('Company deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.company_deleted_successfully'), 'success'); companyEditLog.info('Company deleted successfully'); // Redirect to companies list diff --git a/app/modules/tenancy/static/admin/js/user-create.js b/app/modules/tenancy/static/admin/js/user-create.js index 64ae2f90..2c424de2 100644 --- a/app/modules/tenancy/static/admin/js/user-create.js +++ b/app/modules/tenancy/static/admin/js/user-create.js @@ -26,6 +26,9 @@ function adminUserCreate() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + userCreateLog.info('=== ADMIN USER CREATE PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -85,7 +88,7 @@ function adminUserCreate() { if (!this.validateForm()) { userCreateLog.warn('Validation failed:', this.errors); - Utils.showToast('Please fix the errors before submitting', 'error'); + Utils.showToast(I18n.t('tenancy.messages.please_fix_the_errors_before_submitting'), 'error'); return; } diff --git a/app/modules/tenancy/static/admin/js/user-detail.js b/app/modules/tenancy/static/admin/js/user-detail.js index 1f00b704..ae80a171 100644 --- a/app/modules/tenancy/static/admin/js/user-detail.js +++ b/app/modules/tenancy/static/admin/js/user-detail.js @@ -19,6 +19,9 @@ function adminUserDetail() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + userDetailLog.info('=== USER DETAIL PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -39,7 +42,7 @@ function adminUserDetail() { } else { userDetailLog.error('No user ID in URL'); this.error = 'Invalid user URL'; - Utils.showToast('Invalid user URL', 'error'); + Utils.showToast(I18n.t('tenancy.messages.invalid_user_url'), 'error'); } userDetailLog.info('=== USER DETAIL PAGE INITIALIZATION COMPLETE ==='); @@ -75,7 +78,7 @@ function adminUserDetail() { } catch (error) { window.LogConfig.logError(error, 'Load User Details'); this.error = error.message || 'Failed to load user details'; - Utils.showToast('Failed to load user details', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_user_details'), 'error'); } finally { this.loading = false; } @@ -149,7 +152,7 @@ function adminUserDetail() { window.LogConfig.logApiCall('DELETE', url, null, 'response'); - Utils.showToast('User deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.user_deleted_successfully'), 'success'); userDetailLog.info('User deleted successfully'); // Redirect to users list @@ -167,7 +170,7 @@ function adminUserDetail() { async refresh() { userDetailLog.info('=== USER REFRESH TRIGGERED ==='); await this.loadUser(); - Utils.showToast('User details refreshed', 'success'); + Utils.showToast(I18n.t('tenancy.messages.user_details_refreshed'), 'success'); userDetailLog.info('=== USER REFRESH COMPLETE ==='); } }; diff --git a/app/modules/tenancy/static/admin/js/user-edit.js b/app/modules/tenancy/static/admin/js/user-edit.js index 19ce01b9..8314549e 100644 --- a/app/modules/tenancy/static/admin/js/user-edit.js +++ b/app/modules/tenancy/static/admin/js/user-edit.js @@ -20,6 +20,9 @@ function adminUserEdit() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + userEditLog.info('=== USER EDIT PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -40,14 +43,14 @@ function adminUserEdit() { await this.loadUser(); } else { userEditLog.error('No user ID in URL'); - Utils.showToast('Invalid user URL', 'error'); + Utils.showToast(I18n.t('tenancy.messages.invalid_user_url'), 'error'); setTimeout(() => window.location.href = '/admin/users', 2000); } userEditLog.info('=== USER EDIT PAGE INITIALIZATION COMPLETE ==='); } catch (error) { window.LogConfig.logError(error, 'User Edit Init'); - Utils.showToast('Failed to initialize page', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_initialize_page'), 'error'); } }, @@ -87,7 +90,7 @@ function adminUserEdit() { } catch (error) { window.LogConfig.logError(error, 'Load User'); - Utils.showToast('Failed to load user', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_user'), 'error'); setTimeout(() => window.location.href = '/admin/users', 2000); } finally { this.loadingUser = false; @@ -122,7 +125,7 @@ function adminUserEdit() { window.LogConfig.logPerformance('Update User', duration); this.user = response; - Utils.showToast('User updated successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.user_updated_successfully'), 'success'); userEditLog.info(`User updated successfully in ${duration}ms`, response); } catch (error) { @@ -207,7 +210,7 @@ function adminUserEdit() { window.LogConfig.logApiCall('DELETE', url, null, 'response'); - Utils.showToast('User deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.user_deleted_successfully'), 'success'); userEditLog.info('User deleted successfully'); // Redirect to users list diff --git a/app/modules/tenancy/static/admin/js/users.js b/app/modules/tenancy/static/admin/js/users.js index 96b521d7..9afe68c1 100644 --- a/app/modules/tenancy/static/admin/js/users.js +++ b/app/modules/tenancy/static/admin/js/users.js @@ -36,6 +36,9 @@ function adminUsers() { // Initialization async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + usersLog.info('=== USERS PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -159,7 +162,7 @@ function adminUsers() { } catch (error) { window.LogConfig.logError(error, 'Load Users'); this.error = error.message || 'Failed to load users'; - Utils.showToast('Failed to load users', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_users'), 'error'); } finally { this.loading = false; } @@ -278,14 +281,14 @@ function adminUsers() { await apiClient.delete(url); // ✅ Fixed: lowercase apiClient - Utils.showToast('User deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.user_deleted_successfully'), 'success'); usersLog.info('User deleted successfully'); await this.loadUsers(); await this.loadStats(); } catch (error) { window.LogConfig.logError(error, 'Delete User'); - Utils.showToast('Failed to delete user', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_delete_user'), 'error'); } }, diff --git a/app/modules/tenancy/static/admin/js/vendor-detail.js b/app/modules/tenancy/static/admin/js/vendor-detail.js index 83823611..c6c88286 100644 --- a/app/modules/tenancy/static/admin/js/vendor-detail.js +++ b/app/modules/tenancy/static/admin/js/vendor-detail.js @@ -21,6 +21,9 @@ function adminVendorDetail() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + detailLog.info('=== VENDOR DETAIL PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -45,7 +48,7 @@ function adminVendorDetail() { } else { detailLog.error('No vendor code in URL'); this.error = 'Invalid vendor URL'; - Utils.showToast('Invalid vendor URL', 'error'); + Utils.showToast(I18n.t('tenancy.messages.invalid_vendor_url'), 'error'); } detailLog.info('=== VENDOR DETAIL PAGE INITIALIZATION COMPLETE ==='); @@ -81,7 +84,7 @@ function adminVendorDetail() { } catch (error) { window.LogConfig.logError(error, 'Load Vendor Details'); this.error = error.message || 'Failed to load vendor details'; - Utils.showToast('Failed to load vendor details', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_vendor_details'), 'error'); } finally { this.loading = false; } @@ -144,7 +147,7 @@ function adminVendorDetail() { // Create a new subscription for this vendor async createSubscription() { if (!this.vendor?.id) { - Utils.showToast('No vendor loaded', 'error'); + Utils.showToast(I18n.t('tenancy.messages.no_vendor_loaded'), 'error'); return; } @@ -165,7 +168,7 @@ function adminVendorDetail() { window.LogConfig.logApiCall('POST', url, response, 'response'); this.subscription = response; - Utils.showToast('Subscription created successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.subscription_created_successfully'), 'success'); detailLog.info('Subscription created:', this.subscription); } catch (error) { @@ -198,7 +201,7 @@ function adminVendorDetail() { window.LogConfig.logApiCall('DELETE', url, null, 'response'); - Utils.showToast('Vendor deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.vendor_deleted_successfully'), 'success'); detailLog.info('Vendor deleted successfully'); // Redirect to vendors list @@ -214,7 +217,7 @@ function adminVendorDetail() { async refresh() { detailLog.info('=== VENDOR REFRESH TRIGGERED ==='); await this.loadVendor(); - Utils.showToast('Vendor details refreshed', 'success'); + Utils.showToast(I18n.t('tenancy.messages.vendor_details_refreshed'), 'success'); detailLog.info('=== VENDOR REFRESH COMPLETE ==='); } }; diff --git a/app/modules/tenancy/static/admin/js/vendor-edit.js b/app/modules/tenancy/static/admin/js/vendor-edit.js index b88a69e3..20364268 100644 --- a/app/modules/tenancy/static/admin/js/vendor-edit.js +++ b/app/modules/tenancy/static/admin/js/vendor-edit.js @@ -21,6 +21,9 @@ function adminVendorEdit() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + editLog.info('=== VENDOR EDIT PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -41,14 +44,14 @@ function adminVendorEdit() { await this.loadVendor(); } else { editLog.error('No vendor code in URL'); - Utils.showToast('Invalid vendor URL', 'error'); + Utils.showToast(I18n.t('tenancy.messages.invalid_vendor_url'), 'error'); setTimeout(() => window.location.href = '/admin/vendors', 2000); } editLog.info('=== VENDOR EDIT PAGE INITIALIZATION COMPLETE ==='); } catch (error) { window.LogConfig.logError(error, 'Vendor Edit Init'); - Utils.showToast('Failed to initialize page', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_initialize_page'), 'error'); } }, @@ -96,7 +99,7 @@ function adminVendorEdit() { } catch (error) { window.LogConfig.logError(error, 'Load Vendor'); - Utils.showToast('Failed to load vendor', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_vendor'), 'error'); setTimeout(() => window.location.href = '/admin/vendors', 2000); } finally { this.loadingVendor = false; @@ -131,7 +134,7 @@ function adminVendorEdit() { window.LogConfig.logPerformance('Update Vendor', duration); this.vendor = response; - Utils.showToast('Vendor updated successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.vendor_updated_successfully'), 'success'); editLog.info(`Vendor updated successfully in ${duration}ms`, response); // Optionally redirect back to list @@ -249,7 +252,7 @@ function adminVendorEdit() { window.LogConfig.logApiCall('DELETE', url, response, 'response'); - Utils.showToast('Vendor deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.vendor_deleted_successfully'), 'success'); editLog.info('Vendor deleted successfully'); // Redirect to vendors list @@ -294,7 +297,7 @@ function adminVendorEdit() { this.formData[field] = ''; }); - Utils.showToast('All contact fields reset to company defaults', 'info'); + Utils.showToast(I18n.t('tenancy.messages.all_contact_fields_reset_to_company_defa'), 'info'); }, /** diff --git a/app/modules/tenancy/static/admin/js/vendors.js b/app/modules/tenancy/static/admin/js/vendors.js index 01238202..65c21b27 100644 --- a/app/modules/tenancy/static/admin/js/vendors.js +++ b/app/modules/tenancy/static/admin/js/vendors.js @@ -43,6 +43,9 @@ function adminVendors() { // Initialize async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + vendorsLog.info('=== VENDORS PAGE INITIALIZING ==='); // Prevent multiple initializations @@ -195,7 +198,7 @@ function adminVendors() { } catch (error) { window.LogConfig.logError(error, 'Load Vendors'); this.error = error.message || 'Failed to load vendors'; - Utils.showToast('Failed to load vendors', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_vendors'), 'error'); } finally { this.loading = false; } @@ -298,7 +301,7 @@ function adminVendors() { window.LogConfig.logApiCall('DELETE', url, null, 'response'); - Utils.showToast('Vendor deleted successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.vendor_deleted_successfully'), 'success'); vendorsLog.info('Vendor deleted successfully'); // Reload data @@ -320,7 +323,7 @@ function adminVendors() { await this.loadStats(); vendorsLog.groupEnd(); - Utils.showToast('Vendors list refreshed', 'success'); + Utils.showToast(I18n.t('tenancy.messages.vendors_list_refreshed'), 'success'); vendorsLog.info('=== VENDORS REFRESH COMPLETE ==='); } }; diff --git a/app/modules/tenancy/static/vendor/js/profile.js b/app/modules/tenancy/static/vendor/js/profile.js index f62cd48e..e83d7004 100644 --- a/app/modules/tenancy/static/vendor/js/profile.js +++ b/app/modules/tenancy/static/vendor/js/profile.js @@ -45,6 +45,9 @@ function vendorProfile() { hasChanges: false, async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + vendorProfileLog.info('Profile init() called'); // Guard against multiple initialization @@ -153,7 +156,7 @@ function vendorProfile() { */ async saveProfile() { if (!this.validateForm()) { - Utils.showToast('Please fix the errors before saving', 'error'); + Utils.showToast(I18n.t('tenancy.messages.please_fix_the_errors_before_saving'), 'error'); return; } @@ -161,7 +164,7 @@ function vendorProfile() { try { await apiClient.put(`/vendor/profile`, this.form); - Utils.showToast('Profile updated successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.profile_updated_successfully'), 'success'); vendorProfileLog.info('Profile updated'); this.hasChanges = false; diff --git a/app/modules/tenancy/static/vendor/js/settings.js b/app/modules/tenancy/static/vendor/js/settings.js index 92e4f728..fd8a763c 100644 --- a/app/modules/tenancy/static/vendor/js/settings.js +++ b/app/modules/tenancy/static/vendor/js/settings.js @@ -135,6 +135,9 @@ function vendorSettings() { hasMarketplaceChanges: false, async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + vendorSettingsLog.info('Settings init() called'); // Guard against multiple initialization @@ -315,7 +318,7 @@ function vendorSettings() { await apiClient.put(`/vendor/settings/business-info`, payload); - Utils.showToast('Business info saved', 'success'); + Utils.showToast(I18n.t('tenancy.messages.business_info_saved'), 'success'); vendorSettingsLog.info('Business info updated'); // Reload to get updated inheritance flags @@ -337,7 +340,7 @@ function vendorSettings() { try { await apiClient.put(`/vendor/settings/letzshop`, this.marketplaceForm); - Utils.showToast('Marketplace settings saved', 'success'); + Utils.showToast(I18n.t('tenancy.messages.marketplace_settings_saved'), 'success'); vendorSettingsLog.info('Marketplace settings updated'); this.hasMarketplaceChanges = false; @@ -355,7 +358,7 @@ function vendorSettings() { async testLetzshopUrl(lang) { const url = this.marketplaceForm[`letzshop_csv_url_${lang}`]; if (!url) { - Utils.showToast('Please enter a URL first', 'error'); + Utils.showToast(I18n.t('tenancy.messages.please_enter_a_url_first'), 'error'); return; } @@ -365,7 +368,7 @@ function vendorSettings() { const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); Utils.showToast(`URL appears to be valid`, 'success'); } catch (error) { - Utils.showToast('Could not validate URL - it may still work', 'warning'); + Utils.showToast(I18n.t('tenancy.messages.could_not_validate_url_it_may_still_work'), 'warning'); } finally { this.saving = false; } @@ -406,7 +409,7 @@ function vendorSettings() { try { await apiClient.put(`/vendor/settings/localization`, this.localizationForm); - Utils.showToast('Localization settings saved', 'success'); + Utils.showToast(I18n.t('tenancy.messages.localization_settings_saved'), 'success'); vendorSettingsLog.info('Localization settings updated'); this.hasLocalizationChanges = false; @@ -450,7 +453,7 @@ function vendorSettings() { vendorSettingsLog.info('Loaded email settings'); } catch (error) { vendorSettingsLog.error('Failed to load email settings:', error); - Utils.showToast('Failed to load email settings', 'error'); + Utils.showToast(I18n.t('tenancy.messages.failed_to_load_email_settings'), 'error'); } finally { this.emailSettingsLoading = false; } @@ -500,7 +503,7 @@ function vendorSettings() { async saveEmailSettings() { // Validate required fields if (!this.emailForm.from_email || !this.emailForm.from_name) { - Utils.showToast('From Email and From Name are required', 'error'); + Utils.showToast(I18n.t('tenancy.messages.from_email_and_from_name_are_required'), 'error'); return; } @@ -509,7 +512,7 @@ function vendorSettings() { const response = await apiClient.put('/vendor/email-settings', this.emailForm); if (response.success) { - Utils.showToast('Email settings saved', 'success'); + Utils.showToast(I18n.t('tenancy.messages.email_settings_saved'), 'success'); vendorSettingsLog.info('Email settings updated'); // Update local state @@ -531,12 +534,12 @@ function vendorSettings() { */ async sendTestEmail() { if (!this.testEmailAddress) { - Utils.showToast('Please enter a test email address', 'error'); + Utils.showToast(I18n.t('tenancy.messages.please_enter_a_test_email_address'), 'error'); return; } if (!this.emailSettings?.is_configured) { - Utils.showToast('Please save your email settings first', 'error'); + Utils.showToast(I18n.t('tenancy.messages.please_save_your_email_settings_first'), 'error'); return; } @@ -547,7 +550,7 @@ function vendorSettings() { }); if (response.success) { - Utils.showToast('Test email sent! Check your inbox.', 'success'); + Utils.showToast(I18n.t('tenancy.messages.test_email_sent_check_your_inbox'), 'success'); // Update verification status this.emailSettings.is_verified = true; } else { diff --git a/app/modules/tenancy/static/vendor/js/team.js b/app/modules/tenancy/static/vendor/js/team.js index 86c84c64..517c5514 100644 --- a/app/modules/tenancy/static/vendor/js/team.js +++ b/app/modules/tenancy/static/vendor/js/team.js @@ -64,6 +64,9 @@ function vendorTeam() { ], async init() { + // Load i18n translations + await I18n.loadModule('tenancy'); + vendorTeamLog.info('Team init() called'); // Guard against multiple initialization @@ -149,7 +152,7 @@ function vendorTeam() { */ async sendInvitation() { if (!this.inviteForm.email) { - Utils.showToast('Email is required', 'error'); + Utils.showToast(I18n.t('tenancy.messages.email_is_required'), 'error'); return; } @@ -157,7 +160,7 @@ function vendorTeam() { try { await apiClient.post(`/vendor/team/invite`, this.inviteForm); - Utils.showToast('Invitation sent successfully', 'success'); + Utils.showToast(I18n.t('tenancy.messages.invitation_sent_successfully'), 'success'); vendorTeamLog.info('Invitation sent to:', this.inviteForm.email); this.showInviteModal = false; @@ -195,7 +198,7 @@ function vendorTeam() { this.editForm ); - Utils.showToast('Team member updated', 'success'); + Utils.showToast(I18n.t('tenancy.messages.team_member_updated'), 'success'); vendorTeamLog.info('Updated team member:', this.selectedMember.user_id); this.showEditModal = false; @@ -227,7 +230,7 @@ function vendorTeam() { try { await apiClient.delete(`/vendor/team/members/${this.selectedMember.user_id}`); - Utils.showToast('Team member removed', 'success'); + Utils.showToast(I18n.t('tenancy.messages.team_member_removed'), 'success'); vendorTeamLog.info('Removed team member:', this.selectedMember.user_id); this.showRemoveModal = false; diff --git a/app/templates/admin/base.html b/app/templates/admin/base.html index 52f362eb..661b5d4d 100644 --- a/app/templates/admin/base.html +++ b/app/templates/admin/base.html @@ -109,6 +109,16 @@ + + + + diff --git a/app/templates/public/base.html b/app/templates/public/base.html index 07b54811..fe27f16d 100644 --- a/app/templates/public/base.html +++ b/app/templates/public/base.html @@ -73,13 +73,13 @@

      - {{ _("platform.footer.tagline") }} + {{ _("cms.platform.footer.tagline") }}

      {# Quick Links #}
      -

      {{ _("platform.footer.quick_links") }}

      +

      {{ _("cms.platform.footer.quick_links") }}

        {% if footer_pages %} {% for page in footer_pages %} @@ -217,16 +217,16 @@ {# Platform Links #}
        -

        {{ _("platform.footer.platform") }}

        +

        {{ _("cms.platform.footer.platform") }}

        @@ -234,7 +234,7 @@ {# Contact Info #}
        -

        {{ _("platform.footer.contact") }}

        +

        {{ _("cms.platform.footer.contact") }}

        • support@marketplace.com
        • +1 (555) 123-4567
        • @@ -248,7 +248,7 @@

          - {{ _("platform.footer.copyright", year=2025) }} + {{ _("cms.platform.footer.copyright", year=2025) }}

          {% if legal_pages %} @@ -260,10 +260,10 @@ {% else %} {# Fallback to hardcoded links if no CMS pages #} - {{ _("platform.footer.privacy") }} + {{ _("cms.platform.footer.privacy") }} - {{ _("platform.footer.terms") }} + {{ _("cms.platform.footer.terms") }} {% endif %}
          diff --git a/app/templates/storefront/base.html b/app/templates/storefront/base.html index b55c1a4d..70b5a237 100644 --- a/app/templates/storefront/base.html +++ b/app/templates/storefront/base.html @@ -326,6 +326,16 @@ {# 5. Utilities #} + {# 5b. i18n Support #} + + + {# 6. API Client #} diff --git a/app/templates/vendor/base.html b/app/templates/vendor/base.html index 03800483..4b3fdfa9 100644 --- a/app/templates/vendor/base.html +++ b/app/templates/vendor/base.html @@ -68,6 +68,16 @@ + + + + diff --git a/docs/architecture/menu-management.md b/docs/architecture/menu-management.md index 3355a3aa..d97b5d32 100644 --- a/docs/architecture/menu-management.md +++ b/docs/architecture/menu-management.md @@ -1,24 +1,39 @@ # Menu Management Architecture -The Wizamart platform provides a flexible menu system that supports per-platform and per-user customization. This document explains the menu architecture, configuration options, and how menus integrate with the module system. +The Wizamart platform provides a **module-driven menu system** where each module defines its own menu items. The `MenuDiscoveryService` aggregates menus from all enabled modules, applying visibility configuration and permission filtering. ## Overview ``` ┌─────────────────────────────────────────────────────────────────────────┐ -│ MENU REGISTRY (Source of Truth) │ -│ app/config/menu_registry.py │ +│ MODULE DEFINITIONS (Source of Truth) │ +│ app/modules/*/definition.py │ │ │ +│ Each module defines its menu items per FrontendType: │ │ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ -│ │ ADMIN_MENU_REGISTRY │ │ VENDOR_MENU_REGISTRY │ │ -│ │ (Admin panel menus) │ │ (Vendor dashboard menus) │ │ +│ │ catalog.definition.py │ │ orders.definition.py │ │ +│ │ menus={ADMIN: [...], │ │ menus={ADMIN: [...], │ │ +│ │ VENDOR: [...]} │ │ VENDOR: [...]} │ │ │ └─────────────────────────────┘ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ -│ VISIBILITY CONFIGURATION │ -│ models/database/admin_menu_config.py │ +│ MENU DISCOVERY SERVICE │ +│ app/modules/core/services/menu_discovery_service.py │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 1. Collect menu items from all enabled modules │ │ +│ │ 2. Filter by user permissions (super_admin_only) │ │ +│ │ 3. Apply visibility overrides (AdminMenuConfig) │ │ +│ │ 4. Sort by section/item order │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ VISIBILITY CONFIGURATION │ +│ app/modules/core/models/admin_menu_config.py │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ AdminMenuConfig Table │ │ @@ -32,95 +47,156 @@ The Wizamart platform provides a flexible menu system that supports per-platform ┌─────────────────────────────────────────────────────────────────────────┐ │ FILTERED MENU OUTPUT │ │ │ -│ Registry - Hidden Items - Disabled Modules = Visible Menu Items │ +│ Module Menus - Disabled Modules - Hidden Items = Visible Menu │ └─────────────────────────────────────────────────────────────────────────┘ ``` ## Frontend Types -The system supports two distinct frontend types: +The system supports four distinct frontend types: | Frontend | Description | Users | |----------|-------------|-------| +| `PLATFORM` | Public marketing pages | Unauthenticated visitors | | `ADMIN` | Admin panel | Super admins, platform admins | | `VENDOR` | Vendor dashboard | Vendors on a platform | +| `STOREFRONT` | Customer-facing shop | Shop customers | ```python -from models.database.admin_menu_config import FrontendType +from app.modules.enums import FrontendType # Use in code -FrontendType.ADMIN # "admin" -FrontendType.VENDOR # "vendor" +FrontendType.PLATFORM # "platform" +FrontendType.ADMIN # "admin" +FrontendType.VENDOR # "vendor" +FrontendType.STOREFRONT # "storefront" ``` -## Menu Registry +## Module-Driven Menus -The menu registry (`app/config/menu_registry.py`) is the **single source of truth** for menu structure. The database only stores visibility overrides. +Each module defines its menu items in `definition.py` using dataclasses: -### Admin Menu Structure +### Menu Item Definition ```python -ADMIN_MENU_REGISTRY = { - "frontend_type": FrontendType.ADMIN, - "sections": [ - { - "id": "main", - "label": None, # No header - "items": [ - {"id": "dashboard", "label": "Dashboard", "icon": "home", "url": "/admin/dashboard"}, - ], - }, - { - "id": "superAdmin", - "label": "Super Admin", - "super_admin_only": True, # Only visible to super admins - "items": [ - {"id": "admin-users", "label": "Admin Users", "icon": "shield", "url": "/admin/admin-users"}, - ], - }, - # ... more sections - ], -} +# app/modules/base.py + +@dataclass +class MenuItemDefinition: + """Single menu item definition.""" + id: str # Unique identifier (e.g., "catalog.products") + label_key: str # i18n key for label + icon: str # Lucide icon name + route: str # URL path + order: int = 100 # Sort order within section + is_mandatory: bool = False # Cannot be hidden by user + is_super_admin_only: bool = False # Only visible to super admins + +@dataclass +class MenuSectionDefinition: + """Section containing menu items.""" + id: str # Section identifier + label_key: str # i18n key for section label + icon: str # Section icon + order: int = 100 # Sort order + items: list[MenuItemDefinition] = field(default_factory=list) + is_super_admin_only: bool = False ``` -### Admin Menu Sections +### Example Module Definition -| Section ID | Label | Description | -|------------|-------|-------------| -| `main` | (none) | Dashboard - always at top | -| `superAdmin` | Super Admin | Super admin only tools | -| `platformAdmin` | Platform Administration | Companies, vendors, messages | -| `vendorOps` | Vendor Operations | Products, customers, inventory, orders | -| `marketplace` | Marketplace | Letzshop integration | -| `billing` | Billing & Subscriptions | Tiers, subscriptions, billing history | -| `contentMgmt` | Content Management | Platforms, content pages, themes | -| `devTools` | Developer Tools | Components, icons | -| `platformHealth` | Platform Health | Capacity, testing, code quality | -| `monitoring` | Platform Monitoring | Imports, tasks, logs, notifications | -| `settingsSection` | Platform Settings | General settings, email templates, my menu | +```python +# app/modules/catalog/definition.py -### Vendor Menu Sections +from app.modules.base import ModuleDefinition, MenuSectionDefinition, MenuItemDefinition +from app.modules.enums import FrontendType -| Section ID | Label | Description | -|------------|-------|-------------| -| `main` | (none) | Dashboard, analytics | -| `products` | Products & Inventory | Products, inventory, marketplace import | -| `sales` | Sales & Orders | Orders, Letzshop orders, invoices | -| `customers` | Customers | Customers, messages, notifications | -| `shop` | Shop & Content | Content pages, media library | -| `account` | Account & Settings | Team, profile, billing, settings | +catalog_module = ModuleDefinition( + code="catalog", + name="Product Catalog", + description="Product and category management", + version="1.0.0", + is_core=True, + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="catalog", + label_key="menu.catalog", + icon="package", + order=30, + items=[ + MenuItemDefinition( + id="products", + label_key="menu.products", + icon="box", + route="/admin/products", + order=10, + is_mandatory=True + ), + MenuItemDefinition( + id="categories", + label_key="menu.categories", + icon="folder-tree", + route="/admin/categories", + order=20 + ), + ] + ) + ], + FrontendType.VENDOR: [ + MenuSectionDefinition( + id="products", + label_key="menu.my_products", + icon="package", + order=10, + items=[ + MenuItemDefinition( + id="products", + label_key="menu.products", + icon="box", + route="/vendor/{vendor_code}/products", + order=10, + is_mandatory=True + ), + ] + ) + ], + } +) +``` -### Menu Item Properties +## Menu Discovery Service -Each menu item has these properties: +The `MenuDiscoveryService` aggregates menus from all enabled modules: -| Property | Type | Description | -|----------|------|-------------| -| `id` | string | Unique identifier (used for config and access control) | -| `label` | string | Display text | -| `icon` | string | Heroicons icon name | -| `url` | string | Route URL | -| `super_admin_only` | boolean | (Optional) Only visible to super admins | +```python +from app.modules.core.services.menu_discovery_service import menu_discovery_service + +# Get menu for a frontend type +sections = menu_discovery_service.get_menu_for_frontend( + db=db, + frontend_type=FrontendType.ADMIN, + platform_id=1, + user_id=current_user.id, + is_super_admin=current_user.is_super_admin, +) + +# Get all menu items for configuration UI +all_items = menu_discovery_service.get_all_menu_items( + db=db, + frontend_type=FrontendType.ADMIN, + platform_id=1, +) +``` + +### Discovery Flow + +1. **Collect**: Get menu definitions from all modules in `MODULES` registry +2. **Filter by Module**: Only include menus from enabled modules for the platform +3. **Filter by Permissions**: Remove `super_admin_only` items for non-super admins +4. **Apply Visibility**: Check `AdminMenuConfig` for hidden items +5. **Sort**: Order sections and items by their `order` field +6. **Return**: List of `MenuSectionDefinition` with filtered items ## Visibility Configuration @@ -133,23 +209,16 @@ The system uses an **opt-out model**: ### Mandatory Items -Certain menu items cannot be hidden: +Certain menu items cannot be hidden. These are marked with `is_mandatory=True` in their definition: ```python -MANDATORY_MENU_ITEMS = { - FrontendType.ADMIN: frozenset({ - "dashboard", # Landing page - "companies", # Platform admin core - "vendors", # Platform admin core - "admin-users", # Super admin core - "settings", # Configuration access - "my-menu", # Must be able to undo changes - }), - FrontendType.VENDOR: frozenset({ - "dashboard", # Landing page - "settings", # Configuration access - }), -} +MenuItemDefinition( + id="dashboard", + label_key="menu.dashboard", + icon="home", + route="/admin/dashboard", + is_mandatory=True, # Cannot be hidden +) ``` ### Scope Types @@ -210,6 +279,9 @@ CREATE TABLE admin_menu_configs ( ### Examples ```python +from app.modules.core.models import AdminMenuConfig +from app.modules.enums import FrontendType + # Platform "OMS" hides inventory from admin panel AdminMenuConfig( frontend_type=FrontendType.ADMIN, @@ -237,97 +309,65 @@ AdminMenuConfig( ## Module Integration -Menu items are associated with modules. When a module is disabled for a platform, its menu items are automatically hidden. +### Automatic Menu Discovery -### Module to Menu Item Mapping - -Each module defines its menu items per frontend: +When a module is enabled/disabled for a platform, the menu discovery service automatically includes/excludes its menu items: ```python -# In module definition -billing_module = ModuleDefinition( - code="billing", - menu_items={ - FrontendType.ADMIN: ["subscription-tiers", "subscriptions", "billing-history"], - FrontendType.VENDOR: ["billing", "invoices"], - }, -) -``` +# Module enablement check happens automatically in MenuDiscoveryService +enabled_modules = module_service.get_enabled_module_codes(db, platform_id) -### Menu Filtering Logic - -```python -from app.modules.service import module_service - -# Get available menu items (module-aware) -available_items = module_service.get_module_menu_items( - db, platform_id, FrontendType.ADMIN -) - -# Check if specific menu item's module is enabled -is_available = module_service.is_menu_item_module_enabled( - db, platform_id, "subscription-tiers", FrontendType.ADMIN -) +# Only modules in enabled_modules have their menus included +for module_code, module_def in MODULES.items(): + if module_code in enabled_modules: + # Include this module's menus + pass ``` ### Three-Layer Filtering The final visible menu is computed by three layers: -1. **Registry**: All possible menu items +1. **Module Definitions**: All possible menu items from modules 2. **Module Enablement**: Items from disabled modules are hidden 3. **Visibility Config**: Explicitly hidden items are removed ``` -Final Menu = Registry Items +Final Menu = Module Menu Items - Items from Disabled Modules - Items with is_visible=False in config - Items not matching user role (super_admin_only) ``` -## Helper Functions +## Menu Service Integration -### Getting Menu Items +The `MenuService` provides the interface used by templates: ```python -from app.config.menu_registry import ( - get_all_menu_item_ids, - get_menu_item, - is_super_admin_only_item, - ADMIN_MENU_REGISTRY, - VENDOR_MENU_REGISTRY, +from app.modules.core.services import menu_service + +# Get menu for rendering in templates +menu_data = menu_service.get_menu_for_rendering( + db=db, + frontend_type=FrontendType.ADMIN, + platform_id=platform_id, + user_id=user_id, + is_super_admin=is_super_admin, + vendor_code=vendor_code, # For vendor frontend ) -# Get all item IDs for a frontend -admin_items = get_all_menu_item_ids(FrontendType.ADMIN) -# Returns: {'dashboard', 'admin-users', 'companies', ...} - -# Get specific item details -item = get_menu_item(FrontendType.ADMIN, "subscription-tiers") -# Returns: { -# 'id': 'subscription-tiers', -# 'label': 'Subscription Tiers', -# 'icon': 'tag', -# 'url': '/admin/subscription-tiers', -# 'section_id': 'billing', -# 'section_label': 'Billing & Subscriptions' +# Returns legacy format for template compatibility: +# { +# "frontend_type": "admin", +# "sections": [ +# { +# "id": "main", +# "label": None, +# "items": [{"id": "dashboard", "label": "Dashboard", ...}] +# }, +# ... +# ] # } - -# Check if item requires super admin -is_restricted = is_super_admin_only_item("admin-users") # True -``` - -### Menu Enums - -```python -from app.config.menu_registry import AdminMenuItem, VendorMenuItem - -# Use enums for type safety -AdminMenuItem.DASHBOARD.value # "dashboard" -AdminMenuItem.INVENTORY.value # "inventory" - -VendorMenuItem.PRODUCTS.value # "products" -VendorMenuItem.ANALYTICS.value # "analytics" ``` ## Access Control @@ -380,24 +420,74 @@ Located at `/admin/my-menu`: - Personal preferences that don't affect other users - Useful for hiding rarely-used features +## Adding Menu Items to a Module + +1. **Define menu sections and items** in your module's `definition.py`: + +```python +# app/modules/mymodule/definition.py + +from app.modules.base import ModuleDefinition, MenuSectionDefinition, MenuItemDefinition +from app.modules.enums import FrontendType + +mymodule = ModuleDefinition( + code="mymodule", + name="My Module", + description="Description of my module", + version="1.0.0", + menus={ + FrontendType.ADMIN: [ + MenuSectionDefinition( + id="mymodule", + label_key="mymodule.menu.section", + icon="star", + order=50, # Position in sidebar + items=[ + MenuItemDefinition( + id="mymodule-main", + label_key="mymodule.menu.main", + icon="star", + route="/admin/mymodule", + order=10, + ), + ] + ) + ], + } +) +``` + +2. **Add translation keys** in your module's locales: + +```json +// app/modules/mymodule/locales/en.json +{ + "mymodule.menu.section": "My Module", + "mymodule.menu.main": "Main Page" +} +``` + +3. **The menu is automatically discovered** - no registration needed. + ## Best Practices ### Do -- Use menu item enums (`AdminMenuItem`, `VendorMenuItem`) for type safety -- Check module enablement before showing module-specific menu items -- Respect mandatory items - don't try to hide them -- Use `require_menu_access` dependency to protect routes +- Define menus in module `definition.py` using dataclasses +- Use translation keys (`label_key`) for labels +- Set appropriate `order` values for positioning +- Mark essential items as `is_mandatory=True` +- Use `is_super_admin_only=True` for admin-only features ### Don't -- Hardcode menu item IDs as strings (use enums) +- Hardcode menu item labels (use i18n keys) - Store `is_visible=True` in database (default state, wastes space) - Allow hiding mandatory items via API -- Create menu items without registering them in the registry +- Create menu items outside of module definitions ## Related Documentation -- [Module System](module-system.md) - Module architecture and menu item mapping +- [Module System](module-system.md) - Module architecture and definitions - [Multi-Tenant System](multi-tenant.md) - Platform isolation - [Feature Gating](../implementation/feature-gating-system.md) - Tier-based access diff --git a/docs/architecture/models-structure.md b/docs/architecture/models-structure.md index 7c2e19ee..b8352a31 100644 --- a/docs/architecture/models-structure.md +++ b/docs/architecture/models-structure.md @@ -2,34 +2,26 @@ ## Overview -This project uses a **hybrid architecture** for models and schemas: +This project uses a **module-based architecture** for models and schemas: -1. **CORE models/schemas** in `models/database/` and `models/schema/` - Framework-level entities used across modules -2. **Module models/schemas** in `app/modules//models/` and `app/modules//schemas/` - Domain-specific entities +1. **Infrastructure** in `models/database/` and `models/schema/` - Base classes only +2. **Module models/schemas** in `app/modules//models/` and `app/modules//schemas/` - All domain entities 3. **Inline schemas** defined directly in API route files - Endpoint-specific request/response models ## Directory Structure ``` -models/ # CORE framework models -├── database/ # SQLAlchemy models (ORM) -│ ├── __init__.py # Exports all models + module discovery -│ ├── base.py # Base class, mixins -│ ├── user.py # User authentication -│ ├── vendor.py # Vendor, VendorUser, Role -│ ├── company.py # Company management -│ ├── platform.py # Multi-platform support -│ ├── media.py # MediaFile (cross-cutting) -│ └── ... +models/ # INFRASTRUCTURE ONLY +├── database/ # SQLAlchemy base classes +│ ├── __init__.py # Exports Base, TimestampMixin only +│ └── base.py # Base class, mixins │ -└── schema/ # Pydantic schemas (API validation) - ├── __init__.py - ├── auth.py # Login, tokens, password reset - ├── vendor.py # Vendor CRUD schemas - ├── company.py # Company schemas - └── ... +└── schema/ # Pydantic base classes + ├── __init__.py # Exports base and auth only + ├── base.py # Base schema classes + └── auth.py # Auth schemas (cross-cutting) -app/modules// # Domain modules +app/modules// # Domain modules (ALL domain entities) ├── models/ # Module-specific database models │ ├── __init__.py # Canonical exports │ └── *.py # Model definitions @@ -43,20 +35,28 @@ app/modules// # Domain modules ## Architecture Principles -### 1. CORE vs Module Models +### 1. Infrastructure vs Module Code -**CORE models** (`models/database/`) are framework-level entities used across multiple modules: -- `User` - Authentication, used by all modules -- `Vendor` - Multi-tenant anchor, used by all vendor-scoped modules -- `Company` - Business entity management -- `Platform` - Multi-platform CMS support -- `MediaFile` - File storage, used by catalog, CMS, etc. +**Infrastructure** (`models/database/`, `models/schema/`) provides base classes only: +- `Base` - SQLAlchemy declarative base +- `TimestampMixin` - created_at/updated_at columns +- `BaseModel` patterns in `models/schema/base.py` +- `auth.py` - Authentication schemas (cross-cutting concern) -**Module models** (`app/modules//models/`) are domain-specific: -- `billing/models/` - Feature, SubscriptionTier, VendorSubscription -- `catalog/models/` - Product, ProductTranslation, ProductMedia -- `orders/models/` - Order, OrderItem, Invoice -- `inventory/models/` - Inventory, InventoryTransaction +**Module code** (`app/modules//`) contains all domain-specific entities: + +| Module | Models | Schemas | +|--------|--------|---------| +| `tenancy` | User, Admin, Vendor, Company, Platform, VendorDomain | company, vendor, admin, team, vendor_domain | +| `billing` | Feature, SubscriptionTier, VendorSubscription | billing, subscription | +| `catalog` | Product, ProductTranslation, ProductMedia | catalog, product, vendor_product | +| `orders` | Order, OrderItem, Invoice | order, invoice, order_item_exception | +| `inventory` | Inventory, InventoryTransaction | inventory | +| `cms` | ContentPage, MediaFile, VendorTheme | content_page, media, image, vendor_theme | +| `messaging` | Email, VendorEmailSettings, VendorEmailTemplate, Message, Notification | email, message, notification | +| `customers` | Customer, PasswordResetToken | customer, context | +| `marketplace` | 5 models | 4 schemas | +| `core` | AdminMenuConfig | (inline) | ### 2. Schema Patterns @@ -84,7 +84,7 @@ class InventoryResponse(BaseModel): For schemas only used by a single endpoint or closely related endpoints. ```python -# app/api/v1/admin/platforms.py +# app/modules/tenancy/routes/api/admin_platforms.py class PlatformResponse(BaseModel): """Platform response schema - only used in this file.""" id: int @@ -122,7 +122,6 @@ class VendorPlatform(Base): | Schema used by single endpoint | Inline | In the route file | | Admin-only endpoint schemas | Inline | In the admin route file | | Model not exposed via API | No schema | N/A | -| Cross-module utility schemas | Dedicated file | `models/schema/` | --- @@ -168,74 +167,100 @@ class VendorPlatform(Base): ## Import Guidelines -### Canonical Imports (Preferred) +### Canonical Imports (Required) ```python -# CORE models - from models.database -from models.database import User, Vendor, MediaFile +# Infrastructure - base classes only +from models.database import Base, TimestampMixin +from models.schema.auth import LoginRequest, TokenResponse # Module models - from app.modules..models +from app.modules.tenancy.models import User, Vendor, Company from app.modules.billing.models import Feature, SubscriptionTier from app.modules.catalog.models import Product, ProductMedia from app.modules.orders.models import Order, OrderItem - -# CORE schemas - from models.schema -from models.schema.auth import LoginRequest, TokenResponse +from app.modules.cms.models import MediaFile, VendorTheme +from app.modules.messaging.models import Email, VendorEmailTemplate # Module schemas - from app.modules..schemas +from app.modules.tenancy.schemas import VendorCreate, CompanyResponse +from app.modules.cms.schemas import MediaItemResponse, VendorThemeResponse +from app.modules.messaging.schemas import EmailTemplateResponse from app.modules.inventory.schemas import InventoryCreate, InventoryResponse from app.modules.orders.schemas import OrderResponse ``` -### Legacy Re-exports (Backwards Compatibility) +### Legacy Imports (DEPRECATED) -`models/database/__init__.py` re-exports module models for backwards compatibility: +The following import patterns are deprecated and will cause architecture validation errors: ```python -# These work but prefer canonical imports -from models.database import Product # Re-exported from catalog module -from models.database import Order # Re-exported from orders module +# DEPRECATED - Don't import domain models from models.database +from models.database import User, Vendor # WRONG + +# DEPRECATED - Don't import domain schemas from models.schema +from models.schema.vendor import VendorCreate # WRONG +from models.schema.company import CompanyResponse # WRONG ``` --- -## Gap Analysis: Models vs Schemas +## Module Ownership Reference -Not every database model needs a dedicated schema file. Here's the current alignment: +### Tenancy Module (`app/modules/tenancy/`) -### CORE Framework +**Models:** +- `User` - User authentication and profile +- `Admin` - Admin user management +- `Vendor` - Vendor/merchant entities +- `VendorUser` - Vendor team members +- `Company` - Company management +- `Platform` - Multi-platform support +- `AdminPlatform` - Admin-platform association +- `VendorPlatform` - Vendor-platform association +- `PlatformModule` - Module configuration per platform +- `VendorDomain` - Custom domain configuration -| Database Model | Schema | Notes | -|----------------|--------|-------| -| `user.py` | `auth.py` | Auth schemas cover user operations | -| `vendor.py` | `vendor.py` | Full CRUD schemas | -| `company.py` | `company.py` | Full CRUD schemas | -| `media.py` | `media.py` | Upload/response schemas | -| `platform.py` | Inline | Admin-only, in `platforms.py` route | -| `platform_module.py` | Inline | Admin-only, in `modules.py` route | -| `admin_menu_config.py` | Inline | Admin-only, in `menu_config.py` route | -| `vendor_email_settings.py` | Inline | In `email_settings.py` route | -| `vendor_email_template.py` | None | Internal email service use | -| `vendor_platform.py` | None | Internal association table | +**Schemas:** +- `company.py` - Company CRUD schemas +- `vendor.py` - Vendor CRUD and Letzshop export schemas +- `admin.py` - Admin user and audit log schemas +- `team.py` - Team management and invitation schemas +- `vendor_domain.py` - Domain configuration schemas -### Modules +### CMS Module (`app/modules/cms/`) -| Module | Models | Schemas | Alignment | -|--------|--------|---------|-----------| -| billing | feature, subscription | billing, subscription | ✅ | -| cart | cart | cart | ✅ | -| catalog | product, product_media, product_translation | catalog, product, vendor_product | ✅ | -| checkout | - | checkout | Schema-only (orchestration) | -| cms | content_page | content_page, homepage_sections | ✅ | -| customers | customer, password_reset_token | customer, context | ✅ (password reset uses auth schemas) | -| dev_tools | architecture_scan, test_run | Inline | Admin-only, inline in route files | -| inventory | inventory, inventory_transaction | inventory | ✅ (transaction included in inventory.py) | -| loyalty | 5 models | 5 schemas | ✅ | -| marketplace | 5 models | 4 schemas | ✅ (translation in product schema) | -| messaging | admin_notification, message | message, notification | ✅ | -| orders | order, order_item, invoice | order, invoice, order_item_exception | ✅ | -| payments | - | payment | Schema-only (external APIs) | -| analytics | - | stats | Schema-only (aggregation views) | +**Models:** +- `ContentPage` - CMS content pages +- `MediaFile` - File storage and management +- `VendorTheme` - Theme customization + +**Schemas:** +- `content_page.py` - Content page schemas +- `media.py` - Media upload/response schemas +- `image.py` - Image handling schemas +- `vendor_theme.py` - Theme configuration schemas + +### Messaging Module (`app/modules/messaging/`) + +**Models:** +- `Email` - Email records +- `VendorEmailSettings` - Email configuration +- `VendorEmailTemplate` - Email templates +- `Message` - Internal messages +- `AdminNotification` - Admin notifications + +**Schemas:** +- `email.py` - Email template schemas +- `message.py` - Message schemas +- `notification.py` - Notification schemas + +### Core Module (`app/modules/core/`) + +**Models:** +- `AdminMenuConfig` - Menu visibility configuration + +**Schemas:** (inline in route files) --- @@ -244,16 +269,14 @@ Not every database model needs a dedicated schema file. Here's the current align ### Database Model Checklist 1. **Determine location:** - - Cross-module use → `models/database/` - - Module-specific → `app/modules//models/` + - All domain models → `app/modules//models/` 2. **Create the model file:** ```python # app/modules/mymodule/models/my_entity.py from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship - from app.core.database import Base - from models.database.base import TimestampMixin + from models.database import Base, TimestampMixin class MyEntity(Base, TimestampMixin): __tablename__ = "my_entities" @@ -299,7 +322,7 @@ Not every database model needs a dedicated schema file. Here's the current align 3. **Or use inline schema:** ```python - # app/api/v1/vendor/my_entity.py + # app/modules/mymodule/routes/api/vendor.py from pydantic import BaseModel class MyEntityResponse(BaseModel): @@ -320,8 +343,8 @@ Not every database model needs a dedicated schema file. Here's the current align |-----------|----------------| | Services access DB directly | `from app.modules.X.models import Model` | | APIs validate with schemas | Request/response Pydantic models | -| Reusable schemas | Dedicated files in `schemas/` | +| Reusable schemas | Dedicated files in `app/modules//schemas/` | | Endpoint-specific schemas | Inline in route files | | Internal models | No schema needed | -| CORE models | `models/database/` | -| Module models | `app/modules//models/` | +| All domain models | `app/modules//models/` | +| Infrastructure only | `models/database/` (Base, TimestampMixin only) | diff --git a/main.py b/main.py index c0b47e4b..6e4e525e 100644 --- a/main.py +++ b/main.py @@ -188,13 +188,23 @@ if MODULES_DIR.exists(): for module_dir in sorted(MODULES_DIR.iterdir()): if not module_dir.is_dir(): continue + + module_name = module_dir.name + + # Mount module static files module_static = module_dir / "static" if module_static.exists(): - module_name = module_dir.name mount_path = f"/static/modules/{module_name}" app.mount(mount_path, StaticFiles(directory=str(module_static)), name=f"{module_name}_static") logger.info(f"Mounted module static files: {mount_path} -> {module_static}") + # Mount module locale files (for JS i18n) + module_locales = module_dir / "locales" + if module_locales.exists(): + mount_path = f"/static/modules/{module_name}/locales" + app.mount(mount_path, StaticFiles(directory=str(module_locales)), name=f"{module_name}_locales") + logger.info(f"Mounted module locales: {mount_path} -> {module_locales}") + # Mount main static directory AFTER module statics if STATIC_DIR.exists(): app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") diff --git a/middleware/auth.py b/middleware/auth.py index 5d1487f7..7b9788f3 100644 --- a/middleware/auth.py +++ b/middleware/auth.py @@ -35,7 +35,7 @@ from app.modules.tenancy.exceptions import ( TokenExpiredException, UserNotActiveException, ) -from models.database.user import User +from app.modules.tenancy.models import User logger = logging.getLogger(__name__) diff --git a/middleware/platform_context.py b/middleware/platform_context.py index 43f8cc87..feaf5809 100644 --- a/middleware/platform_context.py +++ b/middleware/platform_context.py @@ -22,7 +22,7 @@ from fastapi import Request from sqlalchemy.orm import Session from app.core.database import get_db -from models.database.platform import Platform +from app.modules.tenancy.models import Platform # Note: We use pure ASGI middleware (not BaseHTTPMiddleware) to enable path rewriting diff --git a/middleware/theme_context.py b/middleware/theme_context.py index e317c293..44f0c750 100644 --- a/middleware/theme_context.py +++ b/middleware/theme_context.py @@ -17,7 +17,7 @@ from sqlalchemy.orm import Session from starlette.middleware.base import BaseHTTPMiddleware from app.core.database import get_db -from models.database.vendor_theme import VendorTheme +from app.modules.cms.models import VendorTheme logger = logging.getLogger(__name__) diff --git a/middleware/vendor_context.py b/middleware/vendor_context.py index 700b9da6..5aa7e6a2 100644 --- a/middleware/vendor_context.py +++ b/middleware/vendor_context.py @@ -23,8 +23,8 @@ from starlette.middleware.base import BaseHTTPMiddleware from app.core.config import settings from app.core.database import get_db -from models.database.vendor import Vendor -from models.database.vendor_domain import VendorDomain +from app.modules.tenancy.models import Vendor +from app.modules.tenancy.models import VendorDomain logger = logging.getLogger(__name__) @@ -319,7 +319,7 @@ class VendorContextManager: ) if is_custom_domain: - from models.database.vendor_domain import VendorDomain + from app.modules.tenancy.models import VendorDomain normalized_domain = VendorDomain.normalize_domain(referer_host) logger.debug( diff --git a/models/__init__.py b/models/__init__.py index 4f62f88e..9cb8bea9 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -4,24 +4,20 @@ # API models (Pydantic) - import the modules, not all classes from . import schema -# Database models (SQLAlchemy) +# Database models (SQLAlchemy) - base only, avoid circular imports from .database.base import Base -from .database.user import User -from .database.vendor import Vendor -# Module-based models -from app.modules.inventory.models import Inventory -from app.modules.marketplace.models import MarketplaceImportJob, MarketplaceProduct -from app.modules.catalog.models import Product +# Note: User, Vendor, and other domain models should be imported from +# their canonical locations in app/modules/*/models/ +# +# Example: +# from app.modules.tenancy.models import User, Vendor +# from app.modules.catalog.models import Product +# from app.modules.inventory.models import Inventory +# from app.modules.marketplace.models import MarketplaceImportJob, MarketplaceProduct -# Export database models for Alembic +# Export database base for Alembic __all__ = [ "Base", - "User", - "MarketplaceProduct", - "Inventory", - "Vendor", - "Product", - "MarketplaceImportJob", - "api", # API models namespace + "schema", # API models namespace ] diff --git a/models/database/__init__.py b/models/database/__init__.py index 88058b0a..799b80df 100644 --- a/models/database/__init__.py +++ b/models/database/__init__.py @@ -1,256 +1,30 @@ # models/database/__init__.py """ -Database models package. +Database models package - Base classes and mixins only. -This package imports all SQLAlchemy models to ensure they are registered -with Base.metadata. This includes: -1. Core models (defined in this directory) -2. Module models (discovered from app/modules//models/) +This package provides the base infrastructure for SQLAlchemy models: +- Base: SQLAlchemy declarative base +- TimestampMixin: Mixin for created_at/updated_at timestamps -Module Model Discovery: -- Modules can define their own models in app/modules//models/ -- These are automatically imported when this package loads -- Module models must use `from app.core.database import Base` +IMPORTANT: Domain models have been migrated to their respective modules: +- Tenancy models: app.modules.tenancy.models +- Catalog models: app.modules.catalog.models +- Orders models: app.modules.orders.models +- Inventory models: app.modules.inventory.models +- Billing models: app.modules.billing.models +- Messaging models: app.modules.messaging.models +- CMS models: app.modules.cms.models +- Marketplace models: app.modules.marketplace.models +- Customers models: app.modules.customers.models +- Dev Tools models: app.modules.dev_tools.models +- Core models: app.modules.core.models + +Import models from their canonical module locations instead of this package. """ -import importlib -import logging -from pathlib import Path - -logger = logging.getLogger(__name__) - -# ============================================================================ -# CORE MODELS (always loaded) -# ============================================================================ - -from .admin import ( - AdminAuditLog, - AdminSession, - AdminSetting, - PlatformAlert, -) -from app.modules.messaging.models import AdminNotification -from .admin_menu_config import AdminMenuConfig, FrontendType, MANDATORY_MENU_ITEMS -from .admin_platform import AdminPlatform -from app.modules.dev_tools.models import ( - ArchitectureScan, - ArchitectureViolation, - ViolationAssignment, - ViolationComment, -) -from .base import Base -from .company import Company -from .platform import Platform -from .platform_module import PlatformModule -from .vendor_platform import VendorPlatform -from app.modules.customers.models import Customer, CustomerAddress -from app.modules.customers.models import PasswordResetToken -from .email import EmailCategory, EmailLog, EmailStatus, EmailTemplate -from .vendor_email_template import VendorEmailTemplate -from .vendor_email_settings import EmailProvider, VendorEmailSettings, PREMIUM_EMAIL_PROVIDERS -from app.modules.billing.models import Feature, FeatureCategory, FeatureCode, FeatureUILocation -from app.modules.inventory.models import Inventory -from app.modules.inventory.models import InventoryTransaction, TransactionType -from app.modules.orders.models import ( - Invoice, - InvoiceStatus, - VATRegime, - VendorInvoiceSettings, -) -from app.modules.marketplace.models import ( - LetzshopFulfillmentQueue, - LetzshopHistoricalImportJob, - LetzshopSyncLog, - VendorLetzshopCredentials, - MarketplaceImportError, - MarketplaceImportJob, - DigitalDeliveryMethod, - MarketplaceProduct, - ProductType, - MarketplaceProductTranslation, -) -from app.modules.messaging.models import ( - Conversation, - ConversationParticipant, - ConversationType, - Message, - MessageAttachment, - ParticipantType, -) -from .media import MediaFile -from app.modules.catalog.models import ProductMedia -from app.modules.marketplace.models import OnboardingStatus, OnboardingStep, VendorOnboarding -from app.modules.orders.models import Order, OrderItem -from app.modules.orders.models import OrderItemException -from app.modules.catalog.models import Product, ProductTranslation -from app.modules.billing.models import ( - AddOnCategory, - AddOnProduct, - BillingHistory, - BillingPeriod, - StripeWebhookEvent, - SubscriptionStatus, - SubscriptionTier, - TierCode, - TIER_LIMITS, - VendorAddOn, - VendorSubscription, -) -from app.modules.dev_tools.models import TestCollection, TestResult, TestRun -from .user import User -from .vendor import Role, Vendor, VendorUser -from .vendor_domain import VendorDomain -from .vendor_theme import VendorTheme - -# ============================================================================ -# MODULE MODELS (dynamically discovered) -# ============================================================================ - - -def _discover_module_models(): - """ - Discover and import models from app/modules//models/ directories. - - This ensures module models are registered with Base.metadata for: - 1. Alembic migrations - 2. SQLAlchemy queries - - Module models must: - - Be in app/modules//models/__init__.py or individual files - - Import Base from app.core.database - """ - modules_dir = Path(__file__).parent.parent.parent / "app" / "modules" - - if not modules_dir.exists(): - return - - for module_dir in sorted(modules_dir.iterdir()): - if not module_dir.is_dir(): - continue - - models_init = module_dir / "models" / "__init__.py" - if models_init.exists(): - module_name = f"app.modules.{module_dir.name}.models" - try: - importlib.import_module(module_name) - logger.debug(f"[Models] Loaded module models: {module_name}") - except ImportError as e: - logger.warning(f"[Models] Failed to import {module_name}: {e}") - - -# Run discovery at import time -_discover_module_models() - -# ============================================================================ -# EXPORTS -# ============================================================================ +from .base import Base, TimestampMixin __all__ = [ - # Admin-specific models - "AdminAuditLog", - "AdminMenuConfig", - "FrontendType", - "AdminNotification", - "AdminPlatform", - "AdminSetting", - "MANDATORY_MENU_ITEMS", - "PlatformAlert", - "AdminSession", - # Architecture/Code Quality - "ArchitectureScan", - "ArchitectureViolation", - "ViolationAssignment", - "ViolationComment", - # Test Runs - "TestRun", - "TestResult", - "TestCollection", - # Base "Base", - # User & Auth - "User", - # Company & Vendor - "Company", - "Vendor", - "VendorUser", - "Role", - "VendorDomain", - "VendorTheme", - # Platform - "Platform", - "PlatformModule", - "VendorPlatform", - # Customer & Auth - "Customer", - "CustomerAddress", - "PasswordResetToken", - # Email - "EmailCategory", - "EmailLog", - "EmailStatus", - "EmailTemplate", - "VendorEmailTemplate", - "VendorEmailSettings", - "EmailProvider", - "PREMIUM_EMAIL_PROVIDERS", - # Features - "Feature", - "FeatureCategory", - "FeatureCode", - "FeatureUILocation", - # Product - Enums - "ProductType", - "DigitalDeliveryMethod", - # Product - Models - "MarketplaceProduct", - "MarketplaceProductTranslation", - "Product", - "ProductTranslation", - # Import - "MarketplaceImportJob", - "MarketplaceImportError", - # Inventory - "Inventory", - "InventoryTransaction", - "TransactionType", - # Media - "MediaFile", - "ProductMedia", - # Invoicing - "Invoice", - "InvoiceStatus", - "VATRegime", - "VendorInvoiceSettings", - # Orders - "Order", - "OrderItem", - "OrderItemException", - # Letzshop Integration - "VendorLetzshopCredentials", - "LetzshopFulfillmentQueue", - "LetzshopSyncLog", - "LetzshopHistoricalImportJob", - # Subscription & Billing - "VendorSubscription", - "SubscriptionStatus", - "SubscriptionTier", - "TierCode", - "TIER_LIMITS", - "AddOnProduct", - "AddOnCategory", - "BillingPeriod", - "VendorAddOn", - "BillingHistory", - "StripeWebhookEvent", - # Messaging - "Conversation", - "ConversationParticipant", - "ConversationType", - "Message", - "MessageAttachment", - "ParticipantType", - # Onboarding - "OnboardingStatus", - "OnboardingStep", - "VendorOnboarding", + "TimestampMixin", ] diff --git a/models/schema/__init__.py b/models/schema/__init__.py index 77491ceb..fd64e3e2 100644 --- a/models/schema/__init__.py +++ b/models/schema/__init__.py @@ -1,23 +1,30 @@ # models/schema/__init__.py -"""API models package - Pydantic models for request/response validation. +"""API models package - Base classes only. -Note: Many schemas have been migrated to their respective modules: +This package provides the base infrastructure for Pydantic schemas: +- BaseModel configuration +- Common response patterns +- Auth schemas (cross-cutting) + +IMPORTANT: Domain schemas have been migrated to their respective modules: +- Tenancy schemas: app.modules.tenancy.schemas +- CMS schemas: app.modules.cms.schemas +- Messaging schemas: app.modules.messaging.schemas - Customer schemas: app.modules.customers.schemas - Order schemas: app.modules.orders.schemas - Inventory schemas: app.modules.inventory.schemas -- Message schemas: app.modules.messaging.schemas - Cart schemas: app.modules.cart.schemas - Marketplace schemas: app.modules.marketplace.schemas - Catalog/Product schemas: app.modules.catalog.schemas - Payment schemas: app.modules.payments.schemas + +Import schemas from their canonical module locations instead of this package. """ -# Import API model modules that remain in legacy location +# Infrastructure schemas that remain here from . import ( auth, base, - email, - vendor, ) # Common imports for convenience @@ -26,6 +33,4 @@ from .base import * # Base Pydantic models __all__ = [ "base", "auth", - "email", - "vendor", ] diff --git a/scripts/add_i18n_module_loading.py b/scripts/add_i18n_module_loading.py new file mode 100644 index 00000000..3d829575 --- /dev/null +++ b/scripts/add_i18n_module_loading.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +""" +Add I18n.loadModule() calls to JS files that use I18n.t(). +This ensures module translations are loaded before use. +""" + +import re +from pathlib import Path +from collections import defaultdict + +PROJECT_ROOT = Path(__file__).parent.parent +MODULES_DIR = PROJECT_ROOT / "app" / "modules" + +# Pattern to find I18n.t('module.xxx') calls and extract module name +I18N_PATTERN = re.compile(r"I18n\.t\(['\"](\w+)\.") + + +def find_modules_used(content: str) -> set[str]: + """Find all modules referenced in I18n.t() calls.""" + return set(I18N_PATTERN.findall(content)) + + +def add_module_loading(js_file: Path): + """Add I18n.loadModule() calls to a JS file.""" + content = js_file.read_text(encoding="utf-8") + + # Find modules used in this file + modules = find_modules_used(content) + if not modules: + return False + + # Check if already has module loading + if "I18n.loadModule(" in content: + return False + + # Find the init() method and add loading there + # Look for common patterns: + # 1. init() { ... } + # 2. async init() { ... } + + init_patterns = [ + # Pattern for "init() {" or "async init() {" + (r"((?:async\s+)?init\s*\(\s*\)\s*\{)", "init"), + # Pattern for "mounted() {" (Vue style) + (r"(mounted\s*\(\s*\)\s*\{)", "mounted"), + ] + + for pattern, method_name in init_patterns: + match = re.search(pattern, content) + if match: + # Generate loading code + load_calls = "\n".join(f" await I18n.loadModule('{m}');" for m in sorted(modules)) + + # If init is not async, we need to make it async + full_match = match.group(1) + if "async" not in full_match: + # Make init async + new_init = full_match.replace(f"{method_name}()", f"async {method_name}()") + content = content.replace(full_match, new_init) + full_match = new_init + + # Add loading after the opening brace + insert_code = f"\n // Load i18n translations\n{load_calls}\n" + content = content.replace(full_match, full_match + insert_code) + + js_file.write_text(content, encoding="utf-8") + return True + + # If no init found, add at file level (for simpler scripts) + # This handles files that don't use Alpine components + if "function " in content or "const " in content: + # Find first function definition + func_match = re.search(r"^(function\s+\w+\s*\([^)]*\)\s*\{)", content, re.MULTILINE) + if func_match: + load_calls = "\n".join(f" await I18n.loadModule('{m}');" for m in sorted(modules)) + + # Make function async if needed + full_match = func_match.group(1) + if "async" not in full_match: + new_func = full_match.replace("function ", "async function ") + content = content.replace(full_match, new_func) + full_match = new_func + + insert_code = f"\n // Load i18n translations\n{load_calls}\n" + content = content.replace(full_match, full_match + insert_code) + + js_file.write_text(content, encoding="utf-8") + return True + + return False + + +def main(): + print("=" * 70) + print("Adding I18n.loadModule() to JS files") + print("=" * 70) + + updated = 0 + skipped = 0 + + # Find all JS files with I18n.t() calls + for js_file in MODULES_DIR.rglob("*.js"): + content = js_file.read_text(encoding="utf-8") + if "I18n.t(" not in content: + continue + + modules = find_modules_used(content) + if not modules: + continue + + rel_path = js_file.relative_to(PROJECT_ROOT) + + if add_module_loading(js_file): + print(f" Updated: {rel_path} (modules: {', '.join(sorted(modules))})") + updated += 1 + else: + print(f" Skipped: {rel_path} (already has loading or no init method)") + skipped += 1 + + print() + print(f"Updated: {updated} files") + print(f"Skipped: {skipped} files") + print("=" * 70) + + +if __name__ == "__main__": + main() diff --git a/scripts/create_dummy_letzshop_order.py b/scripts/create_dummy_letzshop_order.py index 8b620445..51901651 100755 --- a/scripts/create_dummy_letzshop_order.py +++ b/scripts/create_dummy_letzshop_order.py @@ -23,7 +23,9 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from app.core.database import SessionLocal from app.utils.money import cents_to_euros, euros_to_cents -from models.database import Order, OrderItem, Product, Vendor +from app.modules.orders.models import Order, OrderItem +from app.modules.catalog.models import Product +from app.modules.tenancy.models import Vendor def generate_order_number(): diff --git a/scripts/create_landing_page.py b/scripts/create_landing_page.py index f6b4a21d..4b2ca55f 100755 --- a/scripts/create_landing_page.py +++ b/scripts/create_landing_page.py @@ -18,7 +18,7 @@ from sqlalchemy.orm import Session from app.core.database import SessionLocal from app.modules.cms.models import ContentPage -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor def create_landing_page( diff --git a/scripts/debug_historical_import.py b/scripts/debug_historical_import.py deleted file mode 100644 index 70a2310e..00000000 --- a/scripts/debug_historical_import.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 -"""Debug script for historical import issues.""" - -import sys -sys.path.insert(0, ".") - -from app.core.database import SessionLocal -from app.modules.marketplace.services.letzshop.credentials_service import LetzshopCredentialsService -from app.modules.marketplace.models import LetzshopHistoricalImportJob - - -def get_valid_shipment_states(vendor_id: int = 1): - """Query the GraphQL schema to find valid ShipmentStateEnum values.""" - db = SessionLocal() - try: - creds_service = LetzshopCredentialsService(db) - - # Introspection query to get enum values - introspection_query = """ - query { - __type(name: "ShipmentStateEnum") { - name - enumValues { - name - description - } - } - } - """ - - with creds_service.create_client(vendor_id) as client: - print("Querying ShipmentStateEnum values...") - result = client._execute(introspection_query) - - if result and "__type" in result: - type_info = result["__type"] - if type_info: - print(f"\nEnum: {type_info['name']}") - print("Valid values:") - for value in type_info.get("enumValues", []): - print(f" - {value['name']}: {value.get('description', 'No description')}") - else: - print("ShipmentStateEnum type not found") - else: - print(f"Unexpected result: {result}") - - finally: - db.close() - - -def check_import_job_status(): - """Check the status of historical import jobs.""" - db = SessionLocal() - try: - jobs = db.query(LetzshopHistoricalImportJob).order_by( - LetzshopHistoricalImportJob.id.desc() - ).limit(5).all() - - print("\n=== Recent Historical Import Jobs ===") - for job in jobs: - print(f"\nJob {job.id}:") - print(f" Status: {job.status}") - print(f" Phase: {job.current_phase}") - print(f" Page: {job.current_page}") - print(f" Shipments fetched: {job.shipments_fetched}") - print(f" Orders processed: {job.orders_processed}") - print(f" Orders imported: {job.orders_imported}") - print(f" Orders updated: {job.orders_updated}") - print(f" Orders skipped: {job.orders_skipped}") - print(f" Confirmed stats: {job.confirmed_stats}") - print(f" Declined stats: {job.declined_stats}") - print(f" Error: {job.error_message}") - print(f" Started: {job.started_at}") - print(f" Completed: {job.completed_at}") - finally: - db.close() - - -def test_fetch_states(vendor_id: int = 1): - """Test fetching shipments with different states.""" - db = SessionLocal() - try: - creds_service = LetzshopCredentialsService(db) - - # States to test - states_to_test = ["unconfirmed", "confirmed", "declined", "shipped", "rejected"] - - with creds_service.create_client(vendor_id) as client: - for state in states_to_test: - print(f"\nTesting state: {state}") - try: - # Just try to fetch first page - shipments = client.get_all_shipments_paginated( - state=state, - page_size=1, - max_pages=1, - ) - print(f" ✓ Success: {len(shipments)} shipments") - except Exception as e: - print(f" ✗ Error: {e}") - - finally: - db.close() - - -def check_declined_items(vendor_id: int = 1): - """Check for orders with declined inventory units.""" - db = SessionLocal() - try: - from models.database.letzshop import LetzshopOrder - from collections import Counter - - # Get all orders with inventory_units - orders = db.query(LetzshopOrder).filter( - LetzshopOrder.vendor_id == vendor_id, - LetzshopOrder.inventory_units.isnot(None), - ).all() - - # Count all states - state_counts = Counter() - total_units = 0 - - for order in orders: - units = order.inventory_units or [] - for unit in units: - state = unit.get("state", "unknown") - state_counts[state] += 1 - total_units += 1 - - print(f"\n=== Inventory Unit States (all {len(orders)} orders) ===") - print(f" Total units: {total_units}") - print(f"\n State breakdown:") - for state, count in sorted(state_counts.items(), key=lambda x: -x[1]): - print(f" {state}: {count}") - - # Show a sample order with its units - if orders: - sample = orders[0] - print(f"\nSample order #{sample.id} (shipment {sample.letzshop_shipment_id}):") - print(f" Shipment state: {sample.letzshop_state}") - print(f" Sync status: {sample.sync_status}") - if sample.inventory_units: - for i, unit in enumerate(sample.inventory_units[:5]): - print(f" Unit {i+1}: state={unit.get('state')}, id={unit.get('id')}") - - finally: - db.close() - - -if __name__ == "__main__": - print("=== Letzshop Historical Import Debug ===\n") - - print("1. Checking valid shipment states...") - get_valid_shipment_states() - - print("\n\n2. Testing different state values...") - test_fetch_states() - - print("\n\n3. Checking import job status...") - check_import_job_status() - - print("\n\n4. Checking declined items in inventory units...") - check_declined_items() diff --git a/scripts/init_log_settings.py b/scripts/init_log_settings.py index 5c557fde..1a17ee86 100644 --- a/scripts/init_log_settings.py +++ b/scripts/init_log_settings.py @@ -8,7 +8,7 @@ Run this script to create default logging configuration settings. # Import all models to avoid SQLAlchemy relationship issues import models # noqa: F401 from app.core.database import SessionLocal -from models.database.admin import AdminSetting +from app.modules.tenancy.models import AdminSetting def init_log_settings(): diff --git a/scripts/init_production.py b/scripts/init_production.py index 259c788f..33170cf2 100644 --- a/scripts/init_production.py +++ b/scripts/init_production.py @@ -36,8 +36,8 @@ from app.core.database import SessionLocal from app.core.environment import is_production from app.core.permissions import PermissionGroups from middleware.auth import AuthManager -from models.database.admin import AdminSetting -from models.database.user import User +from app.modules.tenancy.models import AdminSetting +from app.modules.tenancy.models import User # ============================================================================= # HELPER FUNCTIONS diff --git a/scripts/migrate_js_i18n.py b/scripts/migrate_js_i18n.py new file mode 100644 index 00000000..8ee4d1d4 --- /dev/null +++ b/scripts/migrate_js_i18n.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +Migrate hardcoded toast messages in JS files to use I18n.t(). +Extracts messages, creates translation keys, and updates both JS and locale files. +""" + +import json +import re +from pathlib import Path +from collections import defaultdict + +PROJECT_ROOT = Path(__file__).parent.parent +MODULES_DIR = PROJECT_ROOT / "app" / "modules" + +# Languages to update +LANGUAGES = ["en", "fr", "de", "lb"] + +# Pattern to match Utils.showToast('message', 'type') or Utils.showToast("message", "type") +TOAST_PATTERN = re.compile(r"Utils\.showToast\(['\"]([^'\"]+)['\"],\s*['\"](\w+)['\"]\)") + +# Map module names to their message namespace +MODULE_MESSAGE_NS = { + "catalog": "catalog", + "orders": "orders", + "customers": "customers", + "inventory": "inventory", + "marketplace": "marketplace", + "tenancy": "tenancy", + "core": "core", + "messaging": "messaging", + "billing": "billing", + "cms": "cms", + "checkout": "checkout", + "cart": "cart", + "dev_tools": "dev_tools", + "monitoring": "monitoring", + "analytics": "analytics", +} + + +def message_to_key(message: str) -> str: + """Convert a message string to a translation key.""" + # Remove special characters and convert to snake_case + key = message.lower() + key = re.sub(r'[^\w\s]', '', key) + key = re.sub(r'\s+', '_', key) + # Truncate if too long + if len(key) > 40: + key = key[:40].rstrip('_') + return key + + +def find_js_files_with_toasts() -> dict[str, list[Path]]: + """Find all JS files with toast messages, grouped by module.""" + files_by_module = defaultdict(list) + + for module_dir in sorted(MODULES_DIR.iterdir()): + if not module_dir.is_dir(): + continue + module_name = module_dir.name + + # Find all JS files in this module + for js_file in module_dir.rglob("*.js"): + content = js_file.read_text(encoding="utf-8") + if "Utils.showToast(" in content: + files_by_module[module_name].append(js_file) + + return dict(files_by_module) + + +def extract_messages_from_file(js_file: Path) -> list[tuple[str, str]]: + """Extract all toast messages from a JS file.""" + content = js_file.read_text(encoding="utf-8") + return TOAST_PATTERN.findall(content) + + +def load_locale_file(module_path: Path, lang: str) -> dict: + """Load a module's locale file.""" + locale_file = module_path / "locales" / f"{lang}.json" + if locale_file.exists(): + with open(locale_file, encoding="utf-8") as f: + return json.load(f) + return {} + + +def save_locale_file(module_path: Path, lang: str, data: dict): + """Save a module's locale file.""" + locale_file = module_path / "locales" / f"{lang}.json" + locale_file.parent.mkdir(parents=True, exist_ok=True) + with open(locale_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + + +def update_js_file(js_file: Path, module_name: str, message_keys: dict[str, str]): + """Update a JS file to use I18n.t() for toast messages.""" + content = js_file.read_text(encoding="utf-8") + original = content + + for message, key in message_keys.items(): + # Replace both single and double quoted versions + full_key = f"{module_name}.messages.{key}" + + # Pattern to match the exact message in Utils.showToast + for quote in ["'", '"']: + old = f"Utils.showToast({quote}{message}{quote}," + new = f"Utils.showToast(I18n.t('{full_key}')," + content = content.replace(old, new) + + if content != original: + js_file.write_text(content, encoding="utf-8") + return True + return False + + +def process_module(module_name: str, js_files: list[Path]) -> dict[str, str]: + """Process all JS files for a module and return message->key mapping.""" + all_messages = {} + + # Extract all unique messages + for js_file in js_files: + messages = extract_messages_from_file(js_file) + for message, msg_type in messages: + if message not in all_messages: + all_messages[message] = message_to_key(message) + + return all_messages + + +def main(): + print("=" * 70) + print("JS i18n Migration Script") + print("=" * 70) + + # Find all JS files with toasts + files_by_module = find_js_files_with_toasts() + + total_files = sum(len(files) for files in files_by_module.values()) + print(f"Found {total_files} JS files with toast messages across {len(files_by_module)} modules") + print() + + for module_name, js_files in sorted(files_by_module.items()): + print(f"\n{'='*70}") + print(f"Module: {module_name} ({len(js_files)} files)") + print("=" * 70) + + module_path = MODULES_DIR / module_name + + # Process all JS files and get message mappings + message_keys = process_module(module_name, js_files) + + if not message_keys: + print(" No messages found") + continue + + print(f" Found {len(message_keys)} unique messages:") + for msg, key in sorted(message_keys.items(), key=lambda x: x[1]): + print(f" {key}: {msg[:50]}{'...' if len(msg) > 50 else ''}") + + # Update locale files for all languages + print(f"\n Updating locale files...") + for lang in LANGUAGES: + locale_data = load_locale_file(module_path, lang) + + # Add messages section if not exists + if "messages" not in locale_data: + locale_data["messages"] = {} + + # Add each message (only add if not already present) + for message, key in message_keys.items(): + if key not in locale_data["messages"]: + # For English, use the original message + # For other languages, we'll use the English as placeholder + locale_data["messages"][key] = message + + save_locale_file(module_path, lang, locale_data) + print(f" Updated: {lang}.json") + + # Update JS files + print(f"\n Updating JS files...") + for js_file in js_files: + if update_js_file(js_file, module_name, message_keys): + rel_path = js_file.relative_to(PROJECT_ROOT) + print(f" Updated: {rel_path}") + + print("\n" + "=" * 70) + print("Migration complete!") + print("\nNext steps:") + print("1. Review the generated message keys in locale files") + print("2. Translate non-English messages (currently using English as placeholder)") + print("3. Test the application to verify toast messages work") + print("=" * 70) + + +if __name__ == "__main__": + main() diff --git a/scripts/seed_demo.py b/scripts/seed_demo.py index da9d92e0..4110635c 100644 --- a/scripts/seed_demo.py +++ b/scripts/seed_demo.py @@ -52,8 +52,8 @@ from app.core.database import SessionLocal from app.core.environment import get_environment, is_production from middleware.auth import AuthManager from app.modules.cms.models import ContentPage -from models.database.admin import PlatformAlert -from models.database.company import Company +from app.modules.tenancy.models import PlatformAlert +from app.modules.tenancy.models import Company from app.modules.customers.models.customer import Customer, CustomerAddress from app.modules.marketplace.models import ( MarketplaceImportJob, @@ -62,10 +62,10 @@ from app.modules.marketplace.models import ( ) from app.modules.orders.models import Order, OrderItem from app.modules.catalog.models import Product -from models.database.user import User -from models.database.vendor import Role, Vendor, VendorUser -from models.database.vendor_domain import VendorDomain -from models.database.vendor_theme import VendorTheme +from app.modules.tenancy.models import User +from app.modules.tenancy.models import Role, Vendor, VendorUser +from app.modules.tenancy.models import VendorDomain +from app.modules.cms.models import VendorTheme SEED_MODE = os.getenv("SEED_MODE", "normal") # normal, minimal, reset FORCE_RESET = os.getenv("FORCE_RESET", "false").lower() in ("true", "1", "yes") diff --git a/scripts/seed_email_templates.py b/scripts/seed_email_templates.py index 9031af7f..3e5795c0 100644 --- a/scripts/seed_email_templates.py +++ b/scripts/seed_email_templates.py @@ -13,7 +13,7 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from app.core.database import get_db -from models.database.email import EmailCategory, EmailTemplate +from app.modules.messaging.models import EmailCategory, EmailTemplate # ============================================================================= diff --git a/scripts/test_logging_system.py b/scripts/test_logging_system.py index 287a2ebb..ff7e8bfb 100644 --- a/scripts/test_logging_system.py +++ b/scripts/test_logging_system.py @@ -62,7 +62,7 @@ def test_logging_endpoints(): print("\n[3] Checking database logs...") try: from app.core.database import SessionLocal - from models.database.admin import ApplicationLog + from app.modules.tenancy.models import ApplicationLog db = SessionLocal() try: diff --git a/scripts/validate_architecture.py b/scripts/validate_architecture.py index 0ffb613a..85c000dc 100755 --- a/scripts/validate_architecture.py +++ b/scripts/validate_architecture.py @@ -4505,7 +4505,10 @@ class ArchitectureValidator: for py_file in schemas_path.glob("*.py"): if py_file.name == "__init__.py": continue - # Allow auth.py (core authentication schemas) + # Allow base.py (base schema classes - infrastructure) + if py_file.name == "base.py": + continue + # Allow auth.py (core authentication schemas - cross-cutting) if py_file.name == "auth.py": continue diff --git a/scripts/verify_setup.py b/scripts/verify_setup.py index e8cb60d9..0dd0c649 100644 --- a/scripts/verify_setup.py +++ b/scripts/verify_setup.py @@ -146,8 +146,8 @@ def verify_model_structure(): from app.modules.inventory.models import Inventory from app.modules.marketplace.models import MarketplaceImportJob, MarketplaceProduct 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 print("[OK] All database models imported successfully") diff --git a/static/locales/de.json b/static/locales/de.json index b87bd667..db3b705d 100644 --- a/static/locales/de.json +++ b/static/locales/de.json @@ -135,257 +135,6 @@ "account": "Konto", "wishlist": "Wunschliste" }, - "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" - }, - "products": { - "title": "Produkte", - "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", - "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" - }, - "orders": { - "title": "Bestellungen", - "order": "Bestellung", - "order_id": "Bestellnummer", - "order_number": "Bestellnummer", - "order_date": "Bestelldatum", - "order_status": "Bestellstatus", - "order_details": "Bestelldetails", - "order_items": "Bestellartikel", - "order_total": "Bestellsumme", - "subtotal": "Zwischensumme", - "shipping": "Versand", - "tax": "Steuer", - "discount": "Rabatt", - "customer": "Kunde", - "shipping_address": "Lieferadresse", - "billing_address": "Rechnungsadresse", - "payment_method": "Zahlungsmethode", - "payment_status": "Zahlungsstatus", - "tracking": "Sendungsverfolgung", - "tracking_number": "Sendungsnummer", - "carrier": "Versanddienstleister", - "no_orders": "Keine Bestellungen gefunden", - "search_orders": "Bestellungen suchen...", - "filter_by_status": "Nach Status filtern", - "status_pending": "Ausstehend", - "status_processing": "In Bearbeitung", - "status_shipped": "Versendet", - "status_delivered": "Zugestellt", - "status_cancelled": "Storniert", - "status_refunded": "Erstattet", - "status_confirmed": "Bestätigt", - "status_rejected": "Abgelehnt", - "confirm_order": "Bestellung bestätigen", - "reject_order": "Bestellung ablehnen", - "set_tracking": "Sendungsverfolgung setzen", - "view_details": "Details ansehen" - }, - "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..." - }, - "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" - }, - "marketplace": { - "title": "Marktplatz", - "import": "Importieren", - "export": "Exportieren", - "sync": "Synchronisieren", - "source": "Quelle", - "source_url": "Quell-URL", - "import_products": "Produkte importieren", - "start_import": "Import starten", - "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", - "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" - } - }, - "team": { - "title": "Team", - "members": "Mitglieder", - "add_member": "Mitglied hinzufügen", - "invite_member": "Mitglied einladen", - "remove_member": "Mitglied entfernen", - "role": "Rolle", - "owner": "Inhaber", - "manager": "Manager", - "editor": "Bearbeiter", - "viewer": "Betrachter", - "permissions": "Berechtigungen", - "pending_invitations": "Ausstehende Einladungen", - "invitation_sent": "Einladung gesendet", - "invitation_accepted": "Einladung angenommen" - }, - "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" - }, "errors": { "generic": "Ein Fehler ist aufgetreten", "not_found": "Nicht gefunden", @@ -414,35 +163,6 @@ "logout_title": "Abmelden bestätigen", "logout_message": "Sind Sie sicher, dass Sie sich abmelden möchten?" }, - "notifications": { - "title": "Benachrichtigungen", - "mark_read": "Als gelesen markieren", - "mark_all_read": "Alle als gelesen markieren", - "no_notifications": "Keine Benachrichtigungen", - "new_order": "Neue Bestellung", - "order_updated": "Bestellung aktualisiert", - "low_stock": "Warnung bei geringem Bestand", - "import_complete": "Import abgeschlossen", - "import_failed": "Import fehlgeschlagen" - }, - "shop": { - "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", - "thank_you": "Vielen Dank für Ihre Bestellung", - "order_confirmation": "Bestellbestätigung" - }, "footer": { "all_rights_reserved": "Alle Rechte vorbehalten", "powered_by": "Unterstützt von" @@ -471,206 +191,5 @@ "time": "HH:mm", "datetime": "DD.MM.YYYY HH:mm", "currency": "{amount} {symbol}" - }, - "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" - }, - "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": "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": "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." - } } } diff --git a/static/locales/fr.json b/static/locales/fr.json index 3c963cb1..783cdba4 100644 --- a/static/locales/fr.json +++ b/static/locales/fr.json @@ -135,257 +135,6 @@ "account": "Compte", "wishlist": "Liste de souhaits" }, - "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" - }, - "products": { - "title": "Produits", - "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", - "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" - }, - "orders": { - "title": "Commandes", - "order": "Commande", - "order_id": "ID de commande", - "order_number": "Numéro de commande", - "order_date": "Date de commande", - "order_status": "Statut de la commande", - "order_details": "Détails de la commande", - "order_items": "Articles de la commande", - "order_total": "Total de la commande", - "subtotal": "Sous-total", - "shipping": "Livraison", - "tax": "Taxe", - "discount": "Remise", - "customer": "Client", - "shipping_address": "Adresse de livraison", - "billing_address": "Adresse de facturation", - "payment_method": "Mode de paiement", - "payment_status": "Statut du paiement", - "tracking": "Suivi", - "tracking_number": "Numéro de suivi", - "carrier": "Transporteur", - "no_orders": "Aucune commande trouvée", - "search_orders": "Rechercher des commandes...", - "filter_by_status": "Filtrer par statut", - "status_pending": "En attente", - "status_processing": "En cours", - "status_shipped": "Expédiée", - "status_delivered": "Livrée", - "status_cancelled": "Annulée", - "status_refunded": "Remboursée", - "status_confirmed": "Confirmée", - "status_rejected": "Rejetée", - "confirm_order": "Confirmer la commande", - "reject_order": "Rejeter la commande", - "set_tracking": "Définir le suivi", - "view_details": "Voir les détails" - }, - "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..." - }, - "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" - }, - "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", - "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" - } - }, - "team": { - "title": "Équipe", - "members": "Membres", - "add_member": "Ajouter un membre", - "invite_member": "Inviter un membre", - "remove_member": "Retirer un membre", - "role": "Rôle", - "owner": "Propriétaire", - "manager": "Gestionnaire", - "editor": "Éditeur", - "viewer": "Lecteur", - "permissions": "Permissions", - "pending_invitations": "Invitations en attente", - "invitation_sent": "Invitation envoyée", - "invitation_accepted": "Invitation acceptée" - }, - "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" - }, "errors": { "generic": "Une erreur s'est produite", "not_found": "Non trouvé", @@ -414,35 +163,6 @@ "logout_title": "Confirmer la déconnexion", "logout_message": "Êtes-vous sûr de vouloir vous déconnecter ?" }, - "notifications": { - "title": "Notifications", - "mark_read": "Marquer comme lu", - "mark_all_read": "Tout marquer comme lu", - "no_notifications": "Aucune notification", - "new_order": "Nouvelle commande", - "order_updated": "Commande mise à jour", - "low_stock": "Alerte stock faible", - "import_complete": "Importation terminée", - "import_failed": "Échec de l'importation" - }, - "shop": { - "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", - "thank_you": "Merci pour votre commande", - "order_confirmation": "Confirmation de commande" - }, "footer": { "all_rights_reserved": "Tous droits réservés", "powered_by": "Propulsé par" @@ -471,206 +191,5 @@ "time": "HH:mm", "datetime": "DD/MM/YYYY HH:mm", "currency": "{amount} {symbol}" - }, - "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" - }, - "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": "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": "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." - } } } diff --git a/static/locales/lb.json b/static/locales/lb.json index 4c3e63f2..9e175402 100644 --- a/static/locales/lb.json +++ b/static/locales/lb.json @@ -135,257 +135,6 @@ "account": "Kont", "wishlist": "Wonschlëscht" }, - "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" - }, - "products": { - "title": "Produkter", - "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", - "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" - }, - "orders": { - "title": "Bestellungen", - "order": "Bestellung", - "order_id": "Bestellungs-ID", - "order_number": "Bestellungsnummer", - "order_date": "Bestellungsdatum", - "order_status": "Bestellungsstatus", - "order_details": "Bestellungsdetailer", - "order_items": "Bestellungsartikelen", - "order_total": "Bestellungstotal", - "subtotal": "Subtotal", - "shipping": "Versand", - "tax": "Steier", - "discount": "Rabatt", - "customer": "Client", - "shipping_address": "Liwweradress", - "billing_address": "Rechnungsadress", - "payment_method": "Bezuelmethod", - "payment_status": "Bezuelstatus", - "tracking": "Tracking", - "tracking_number": "Trackingnummer", - "carrier": "Transporteur", - "no_orders": "Keng Bestellunge fonnt", - "search_orders": "Bestellunge sichen...", - "filter_by_status": "No Status filteren", - "status_pending": "Aussteesend", - "status_processing": "A Veraarbechtung", - "status_shipped": "Verschéckt", - "status_delivered": "Geliwwert", - "status_cancelled": "Annuléiert", - "status_refunded": "Rembourséiert", - "status_confirmed": "Bestätegt", - "status_rejected": "Ofgeleent", - "confirm_order": "Bestellung bestätegen", - "reject_order": "Bestellung oflehnen", - "set_tracking": "Tracking setzen", - "view_details": "Detailer kucken" - }, - "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..." - }, - "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" - }, - "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", - "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", - "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" - } - }, - "team": { - "title": "Team", - "members": "Memberen", - "add_member": "Member derbäisetzen", - "invite_member": "Member invitéieren", - "remove_member": "Member ewechhuelen", - "role": "Roll", - "owner": "Proprietär", - "manager": "Manager", - "editor": "Editeur", - "viewer": "Betruechter", - "permissions": "Rechter", - "pending_invitations": "Aussteesend Invitatiounen", - "invitation_sent": "Invitatioun geschéckt", - "invitation_accepted": "Invitatioun ugeholl" - }, - "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" - }, "errors": { "generic": "E Feeler ass opgetrueden", "not_found": "Net fonnt", @@ -414,35 +163,6 @@ "logout_title": "Ofmellen bestätegen", "logout_message": "Sidd Dir sécher datt Dir Iech ofmelle wëllt?" }, - "notifications": { - "title": "Notifikatiounen", - "mark_read": "Als gelies markéieren", - "mark_all_read": "Alles als gelies markéieren", - "no_notifications": "Keng Notifikatiounen", - "new_order": "Nei Bestellung", - "order_updated": "Bestellung aktualiséiert", - "low_stock": "Niddreg Lager Alarm", - "import_complete": "Import fäerdeg", - "import_failed": "Import feelgeschloen" - }, - "shop": { - "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", - "thank_you": "Merci fir Är Bestellung", - "order_confirmation": "Bestellungsbestätegung" - }, "footer": { "all_rights_reserved": "All Rechter reservéiert", "powered_by": "Ënnerstëtzt vun" @@ -471,206 +191,5 @@ "time": "HH:mm", "datetime": "DD.MM.YYYY HH:mm", "currency": "{amount} {symbol}" - }, - "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" - }, - "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": "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": "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." - } } } diff --git a/static/shared/js/i18n.js b/static/shared/js/i18n.js new file mode 100644 index 00000000..ecb1165e --- /dev/null +++ b/static/shared/js/i18n.js @@ -0,0 +1,158 @@ +/** + * JavaScript i18n Support + * + * Loads translations from module locale files for use in JavaScript. + * Provides the same translation interface as the Python/Jinja2 system. + * + * Usage: + * // Initialize with language and modules to preload + * await I18n.init('en', ['catalog', 'orders']); + * + * // Or load modules later + * await I18n.loadModule('inventory'); + * + * // Translate + * const message = I18n.t('catalog.messages.product_created'); + * const withVars = I18n.t('common.welcome', { name: 'John' }); + */ +const I18n = { + _translations: {}, + _language: 'en', + _loaded: new Set(), + _loading: new Map(), // Track in-progress loads + + /** + * Initialize with language (call once on page load) + * @param {string} language - Language code (en, fr, de, lb) + * @param {string[]} modules - Optional array of modules to preload + */ + async init(language = 'en', modules = []) { + this._language = language; + // Load shared translations first + await this.loadShared(); + // Preload any specified modules + if (modules && modules.length > 0) { + await Promise.all(modules.map(m => this.loadModule(m))); + } + }, + + /** + * Load shared/common translations from static/locales + */ + async loadShared() { + if (this._loaded.has('shared')) return; + + try { + const response = await fetch(`/static/locales/${this._language}.json`); + if (response.ok) { + const data = await response.json(); + this._translations = { ...this._translations, ...data }; + this._loaded.add('shared'); + } + } catch (e) { + console.warn('[i18n] Failed to load shared translations:', e); + } + }, + + /** + * Load module-specific translations + * @param {string} module - Module name (e.g., 'catalog', 'orders') + */ + async loadModule(module) { + if (this._loaded.has(module)) return; + + try { + const response = await fetch(`/static/modules/${module}/locales/${this._language}.json`); + if (response.ok) { + const data = await response.json(); + // Namespace under module code (matching Python i18n behavior) + this._translations[module] = data; + this._loaded.add(module); + } + } catch (e) { + console.warn(`[i18n] Failed to load ${module} translations:`, e); + } + }, + + /** + * Get translation by key path + * @param {string} key - Dot-notation key (e.g., 'catalog.messages.product_created') + * @param {object} vars - Variables for interpolation + * @returns {string} Translated string or key if not found + */ + t(key, vars = {}) { + const keys = key.split('.'); + let value = this._translations; + + for (const k of keys) { + if (value && typeof value === 'object' && k in value) { + value = value[k]; + } else { + console.warn(`[i18n] Missing translation: ${key}`); + return key; + } + } + + if (typeof value !== 'string') return key; + + // Interpolate variables: {name} -> value + return value.replace(/\{(\w+)\}/g, (match, name) => { + return vars[name] !== undefined ? vars[name] : match; + }); + }, + + /** + * Check if a translation key exists + * @param {string} key - Dot-notation key + * @returns {boolean} True if key exists + */ + has(key) { + const keys = key.split('.'); + let value = this._translations; + + for (const k of keys) { + if (value && typeof value === 'object' && k in value) { + value = value[k]; + } else { + return false; + } + } + + return typeof value === 'string'; + }, + + /** + * Get current language + * @returns {string} Current language code + */ + getLanguage() { + return this._language; + }, + + /** + * Change language (reloads all loaded modules) + * @param {string} language - New language code + */ + async setLanguage(language) { + if (language === this._language) return; + + const loadedModules = [...this._loaded]; + this._language = language; + this._translations = {}; + this._loaded.clear(); + + // Reload all previously loaded modules + for (const module of loadedModules) { + if (module === 'shared') { + await this.loadShared(); + } else { + await this.loadModule(module); + } + } + } +}; + +// Export for module usage +if (typeof window !== 'undefined') { + window.I18n = I18n; +} diff --git a/tests/fixtures/admin_platform_fixtures.py b/tests/fixtures/admin_platform_fixtures.py index d7077dcb..9d4f6971 100644 --- a/tests/fixtures/admin_platform_fixtures.py +++ b/tests/fixtures/admin_platform_fixtures.py @@ -9,9 +9,9 @@ import uuid import pytest -from models.database.admin_platform import AdminPlatform -from models.database.platform import Platform -from models.database.user import User +from app.modules.tenancy.models import AdminPlatform +from app.modules.tenancy.models import Platform +from app.modules.tenancy.models import User @pytest.fixture diff --git a/tests/fixtures/auth_fixtures.py b/tests/fixtures/auth_fixtures.py index f8901242..07c55d9e 100644 --- a/tests/fixtures/auth_fixtures.py +++ b/tests/fixtures/auth_fixtures.py @@ -11,7 +11,7 @@ import uuid import pytest from middleware.auth import AuthManager -from models.database.user import User +from app.modules.tenancy.models import User @pytest.fixture(scope="session") diff --git a/tests/fixtures/module_fixtures.py b/tests/fixtures/module_fixtures.py index e467e6bb..4745571e 100644 --- a/tests/fixtures/module_fixtures.py +++ b/tests/fixtures/module_fixtures.py @@ -11,8 +11,8 @@ from datetime import datetime, timezone import pytest -from models.database.platform import Platform -from models.database.platform_module import PlatformModule +from app.modules.tenancy.models import Platform +from app.modules.tenancy.models import PlatformModule @pytest.fixture diff --git a/tests/fixtures/vendor_fixtures.py b/tests/fixtures/vendor_fixtures.py index b1f7a5f7..1fe112d0 100644 --- a/tests/fixtures/vendor_fixtures.py +++ b/tests/fixtures/vendor_fixtures.py @@ -10,10 +10,10 @@ import uuid import pytest -from models.database.company import Company +from app.modules.tenancy.models import Company from app.modules.inventory.models import Inventory from app.modules.catalog.models import Product -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor @pytest.fixture @@ -71,7 +71,7 @@ def test_vendor(db, test_company): @pytest.fixture def test_vendor_with_vendor_user(db, test_vendor_user): """Create a vendor owned by a vendor user (for testing vendor API endpoints).""" - from models.database.vendor import VendorUser, VendorUserType + from app.modules.tenancy.models import VendorUser, VendorUserType unique_id = str(uuid.uuid4())[:8].upper() diff --git a/tests/integration/api/v1/admin/test_admin_users.py b/tests/integration/api/v1/admin/test_admin_users.py index d553f723..626501a6 100644 --- a/tests/integration/api/v1/admin/test_admin_users.py +++ b/tests/integration/api/v1/admin/test_admin_users.py @@ -147,7 +147,7 @@ class TestAdminUsersPlatformAssignmentAPI: self, client, super_admin_headers, db, test_platform_admin, test_platform, test_super_admin ): """Test removing an admin from a platform.""" - from models.database.admin_platform import AdminPlatform + from app.modules.tenancy.models import AdminPlatform # First create an assignment assignment = AdminPlatform( @@ -172,7 +172,7 @@ class TestAdminUsersPlatformAssignmentAPI: self, client, super_admin_headers, db, test_platform_admin, test_platform, test_super_admin ): """Test getting platforms for an admin.""" - from models.database.admin_platform import AdminPlatform + from app.modules.tenancy.models import AdminPlatform # Create assignment assignment = AdminPlatform( @@ -220,7 +220,7 @@ class TestAdminUsersSuperAdminToggleAPI: self, client, super_admin_headers, db, auth_manager ): """Test demoting a super admin to platform admin.""" - from models.database.user import User + from app.modules.tenancy.models import User # Create another super admin to demote another_super = User( @@ -270,7 +270,7 @@ class TestAdminAuthPlatformSelectionAPI: self, client, db, test_platform_admin, test_platform, test_super_admin, auth_manager ): """Test getting accessible platforms as platform admin.""" - from models.database.admin_platform import AdminPlatform + from app.modules.tenancy.models import AdminPlatform # Create assignment assignment = AdminPlatform( @@ -310,7 +310,7 @@ class TestAdminAuthPlatformSelectionAPI: self, client, db, test_platform_admin, test_platform, test_super_admin ): """Test selecting a platform as platform admin.""" - from models.database.admin_platform import AdminPlatform + from app.modules.tenancy.models import AdminPlatform # Create assignment assignment = AdminPlatform( diff --git a/tests/integration/api/v1/admin/test_email_settings.py b/tests/integration/api/v1/admin/test_email_settings.py index ab79ac2d..cf75e93e 100644 --- a/tests/integration/api/v1/admin/test_email_settings.py +++ b/tests/integration/api/v1/admin/test_email_settings.py @@ -3,7 +3,7 @@ import pytest -from models.database.admin import AdminSetting +from app.modules.tenancy.models import AdminSetting # ============================================================================= diff --git a/tests/integration/api/v1/modules/test_module_access.py b/tests/integration/api/v1/modules/test_module_access.py index 10b04a05..7dbe8986 100644 --- a/tests/integration/api/v1/modules/test_module_access.py +++ b/tests/integration/api/v1/modules/test_module_access.py @@ -11,7 +11,7 @@ Tests verify that: import pytest -from models.database.platform import Platform +from app.modules.tenancy.models import Platform @pytest.mark.integration diff --git a/tests/integration/api/v1/public/test_letzshop_vendors.py b/tests/integration/api/v1/public/test_letzshop_vendors.py index 70c31287..ddb939a2 100644 --- a/tests/integration/api/v1/public/test_letzshop_vendors.py +++ b/tests/integration/api/v1/public/test_letzshop_vendors.py @@ -6,14 +6,14 @@ Tests the /api/v1/public/letzshop-vendors/* endpoints. import pytest -from models.database.vendor import Vendor -from models.database.company import Company +from app.modules.tenancy.models import Vendor +from app.modules.tenancy.models import Company @pytest.fixture def test_owner_user(db, auth_manager): """Create a test owner user for the company.""" - from models.database.user import User + from app.modules.tenancy.models import User user = User( email="owner@test.com", diff --git a/tests/integration/api/v1/public/test_signup.py b/tests/integration/api/v1/public/test_signup.py index 76105c36..6883e45b 100644 --- a/tests/integration/api/v1/public/test_signup.py +++ b/tests/integration/api/v1/public/test_signup.py @@ -8,10 +8,10 @@ from unittest.mock import MagicMock, patch import pytest -from models.database.company import Company +from app.modules.tenancy.models import Company from app.modules.billing.models import TierCode -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 @pytest.fixture diff --git a/tests/integration/api/v1/vendor/test_dashboard.py b/tests/integration/api/v1/vendor/test_dashboard.py index 537fb825..ffb30847 100644 --- a/tests/integration/api/v1/vendor/test_dashboard.py +++ b/tests/integration/api/v1/vendor/test_dashboard.py @@ -105,7 +105,7 @@ class TestVendorDashboardAPI: """Test dashboard stats for user not associated with any vendor""" import uuid - from models.database.user import User + from app.modules.tenancy.models import User # Create vendor user without vendor association hashed_password = auth_manager.hash_password("testpass123") @@ -136,7 +136,7 @@ class TestVendorDashboardAPI: """Test dashboard stats for inactive vendor""" import uuid - from models.database.vendor import Vendor, VendorUser + from app.modules.tenancy.models import Vendor, VendorUser # Create inactive vendor unique_code = f"INACTIVE_{uuid.uuid4().hex[:8].upper()}" diff --git a/tests/integration/api/v1/vendor/test_email_settings.py b/tests/integration/api/v1/vendor/test_email_settings.py index 836334e4..44b55002 100644 --- a/tests/integration/api/v1/vendor/test_email_settings.py +++ b/tests/integration/api/v1/vendor/test_email_settings.py @@ -4,7 +4,7 @@ import pytest from datetime import datetime, timezone -from models.database import VendorEmailSettings +from app.modules.messaging.models import VendorEmailSettings # ============================================================================= diff --git a/tests/integration/middleware/conftest.py b/tests/integration/middleware/conftest.py index 71f06b64..46599ecd 100644 --- a/tests/integration/middleware/conftest.py +++ b/tests/integration/middleware/conftest.py @@ -19,10 +19,10 @@ from fastapi.testclient import TestClient from app.core.database import get_db from main import app -from models.database.company import Company -from models.database.vendor import Vendor -from models.database.vendor_domain import VendorDomain -from models.database.vendor_theme import VendorTheme +from app.modules.tenancy.models import Company +from app.modules.tenancy.models import Vendor +from app.modules.tenancy.models import VendorDomain +from app.modules.cms.models import VendorTheme # Register test routes for middleware tests from tests.integration.middleware.middleware_test_routes import ( diff --git a/tests/integration/middleware/test_theme_loading_flow.py b/tests/integration/middleware/test_theme_loading_flow.py index 5ff84d88..2ad3ad51 100644 --- a/tests/integration/middleware/test_theme_loading_flow.py +++ b/tests/integration/middleware/test_theme_loading_flow.py @@ -171,7 +171,7 @@ class TestThemeLoadingFlow: database. This test verifies the theme loading mechanism when a custom domain is used. """ - from models.database.vendor_theme import VendorTheme + from app.modules.cms.models import VendorTheme # Add theme to custom domain vendor theme = VendorTheme( diff --git a/tests/unit/middleware/test_auth.py b/tests/unit/middleware/test_auth.py index bb7e0227..f8ffd2f2 100644 --- a/tests/unit/middleware/test_auth.py +++ b/tests/unit/middleware/test_auth.py @@ -28,7 +28,7 @@ from app.modules.tenancy.exceptions import ( UserNotActiveException, ) from middleware.auth import AuthManager -from models.database.user import User +from app.modules.tenancy.models import User @pytest.mark.unit diff --git a/tests/unit/models/database/test_admin_platform.py b/tests/unit/models/database/test_admin_platform.py index 77bcb27f..f4c4676e 100644 --- a/tests/unit/models/database/test_admin_platform.py +++ b/tests/unit/models/database/test_admin_platform.py @@ -8,7 +8,7 @@ Tests the admin-platform junction table model and its relationships. import pytest from sqlalchemy.exc import IntegrityError -from models.database.admin_platform import AdminPlatform +from app.modules.tenancy.models import AdminPlatform @pytest.mark.unit @@ -68,7 +68,7 @@ class TestAdminPlatformModel: self, db, auth_manager, test_platform, test_super_admin ): """Test that deleting user cascades to admin platform assignments.""" - from models.database.user import User + from app.modules.tenancy.models import User # Create a temporary admin temp_admin = User( diff --git a/tests/unit/models/database/test_team.py b/tests/unit/models/database/test_team.py index ac96bbfd..9ed049a1 100644 --- a/tests/unit/models/database/test_team.py +++ b/tests/unit/models/database/test_team.py @@ -3,7 +3,7 @@ import pytest -from models.database.vendor import Role, Vendor, VendorUser +from app.modules.tenancy.models import Role, Vendor, VendorUser @pytest.mark.unit diff --git a/tests/unit/models/database/test_user.py b/tests/unit/models/database/test_user.py index b174273d..afa344db 100644 --- a/tests/unit/models/database/test_user.py +++ b/tests/unit/models/database/test_user.py @@ -4,7 +4,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from models.database.user import User +from app.modules.tenancy.models import User @pytest.mark.unit diff --git a/tests/unit/models/database/test_vendor.py b/tests/unit/models/database/test_vendor.py index 060fb28c..e527cd0f 100644 --- a/tests/unit/models/database/test_vendor.py +++ b/tests/unit/models/database/test_vendor.py @@ -4,7 +4,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from models.database.vendor import Vendor +from app.modules.tenancy.models import Vendor @pytest.mark.unit diff --git a/tests/unit/models/schema/test_vendor.py b/tests/unit/models/schema/test_vendor.py index fe2a379e..9284389d 100644 --- a/tests/unit/models/schema/test_vendor.py +++ b/tests/unit/models/schema/test_vendor.py @@ -4,7 +4,7 @@ import pytest from pydantic import ValidationError -from models.schema.vendor import ( +from app.modules.tenancy.schemas.vendor import ( VendorCreate, VendorDetailResponse, VendorListResponse, diff --git a/tests/unit/services/test_admin_notification_service.py b/tests/unit/services/test_admin_notification_service.py index 012fb79b..37951a90 100644 --- a/tests/unit/services/test_admin_notification_service.py +++ b/tests/unit/services/test_admin_notification_service.py @@ -16,7 +16,7 @@ from app.modules.messaging.services.admin_notification_service import ( Severity, ) from app.modules.messaging.models import AdminNotification -from models.database.admin import PlatformAlert +from app.modules.tenancy.models import PlatformAlert @pytest.fixture diff --git a/tests/unit/services/test_admin_platform_service.py b/tests/unit/services/test_admin_platform_service.py index 910b1f80..fc977a0b 100644 --- a/tests/unit/services/test_admin_platform_service.py +++ b/tests/unit/services/test_admin_platform_service.py @@ -123,7 +123,7 @@ class TestAdminPlatformServiceAssign: self, db, test_platform_admin, test_platform, test_super_admin ): """Test reactivating an inactive assignment.""" - from models.database.admin_platform import AdminPlatform + from app.modules.tenancy.models import AdminPlatform service = AdminPlatformService() @@ -157,7 +157,7 @@ class TestAdminPlatformServiceRemove: self, db, test_platform_admin, test_platform, test_super_admin ): """Test successfully removing an admin from a platform.""" - from models.database.admin_platform import AdminPlatform + from app.modules.tenancy.models import AdminPlatform service = AdminPlatformService() @@ -208,7 +208,7 @@ class TestAdminPlatformServiceQueries: self, db, test_platform_admin, test_platform, another_platform, test_super_admin ): """Test getting platforms for an admin.""" - from models.database.admin_platform import AdminPlatform + from app.modules.tenancy.models import AdminPlatform service = AdminPlatformService() @@ -242,8 +242,8 @@ class TestAdminPlatformServiceQueries: self, db, test_platform_admin, test_platform, test_super_admin, auth_manager ): """Test getting admins for a platform.""" - from models.database.admin_platform import AdminPlatform - from models.database.user import User + from app.modules.tenancy.models import AdminPlatform + from app.modules.tenancy.models import User service = AdminPlatformService() @@ -281,7 +281,7 @@ class TestAdminPlatformServiceQueries: self, db, test_platform_admin, test_platform, another_platform, test_super_admin ): """Test getting admin assignments with platform details.""" - from models.database.admin_platform import AdminPlatform + from app.modules.tenancy.models import AdminPlatform service = AdminPlatformService() @@ -330,7 +330,7 @@ class TestAdminPlatformServiceSuperAdmin: self, db, test_super_admin, auth_manager ): """Test demoting super admin to platform admin.""" - from models.database.user import User + from app.modules.tenancy.models import User service = AdminPlatformService() diff --git a/tests/unit/services/test_admin_service.py b/tests/unit/services/test_admin_service.py index a0437202..d057f100 100644 --- a/tests/unit/services/test_admin_service.py +++ b/tests/unit/services/test_admin_service.py @@ -12,7 +12,7 @@ from app.modules.tenancy.exceptions import ( ) from app.modules.tenancy.services.admin_service import AdminService from app.modules.analytics.services.stats_service import stats_service -from models.schema.vendor import VendorCreate +from app.modules.tenancy.schemas.vendor import VendorCreate @pytest.mark.unit @@ -56,7 +56,7 @@ class TestAdminService: def test_toggle_user_status_activate(self, db, test_user, test_admin): """Test activating a user""" - from models.database.user import User + from app.modules.tenancy.models import User # Re-query user to get fresh instance user_to_deactivate = db.query(User).filter(User.id == test_user.id).first() @@ -122,7 +122,7 @@ class TestAdminService: def test_verify_vendor_mark_verified(self, db, test_vendor): """Test marking vendor as verified""" - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor # Re-query vendor to get fresh instance vendor_to_unverify = ( @@ -373,7 +373,7 @@ class TestAdminServiceVendorCreation: assert vendor.vendor_code == vendor_data.vendor_code.upper() # Verify platform assignment - from models.database.vendor_platform import VendorPlatform + from app.modules.tenancy.models import VendorPlatform assignment = ( db.query(VendorPlatform) @@ -408,7 +408,7 @@ class TestAdminServiceVendorCreation: assert vendor is not None # Verify both platform assignments - from models.database.vendor_platform import VendorPlatform + from app.modules.tenancy.models import VendorPlatform assignments = ( db.query(VendorPlatform) @@ -442,7 +442,7 @@ class TestAdminServiceVendorCreation: assert vendor is not None # Verify no platform assignments created - from models.database.vendor_platform import VendorPlatform + from app.modules.tenancy.models import VendorPlatform assignments = ( db.query(VendorPlatform) diff --git a/tests/unit/services/test_auth_service.py b/tests/unit/services/test_auth_service.py index b18a52e8..483e76c1 100644 --- a/tests/unit/services/test_auth_service.py +++ b/tests/unit/services/test_auth_service.py @@ -77,7 +77,7 @@ class TestAuthService: def test_login_user_inactive_user(self, db, test_user): """Test login fails for inactive user.""" - from models.database.user import User + from app.modules.tenancy.models import User # Re-query user and deactivate user = db.query(User).filter(User.id == test_user.id).first() diff --git a/tests/unit/services/test_content_page_service.py b/tests/unit/services/test_content_page_service.py index 27957b9a..857662ba 100644 --- a/tests/unit/services/test_content_page_service.py +++ b/tests/unit/services/test_content_page_service.py @@ -330,7 +330,7 @@ class TestContentPageServiceVendorMethods: self, db, vendor_about_page, other_company ): """Test updating vendor page with wrong vendor raises exception.""" - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor # Create another vendor unique_id = str(uuid.uuid4())[:8] @@ -374,7 +374,7 @@ class TestContentPageServiceVendorMethods: self, db, vendor_about_page, other_company ): """Test deleting vendor page with wrong vendor raises exception.""" - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor # Create another vendor unique_id = str(uuid.uuid4())[:8] diff --git a/tests/unit/services/test_email_service.py b/tests/unit/services/test_email_service.py index 86433d06..fc4a17eb 100644 --- a/tests/unit/services/test_email_service.py +++ b/tests/unit/services/test_email_service.py @@ -13,7 +13,7 @@ from app.modules.messaging.services.email_service import ( SMTPProvider, get_provider, ) -from models.database.email import EmailCategory, EmailLog, EmailStatus, EmailTemplate +from app.modules.messaging.models import EmailCategory, EmailLog, EmailStatus, EmailTemplate @pytest.mark.unit diff --git a/tests/unit/services/test_inventory_service.py b/tests/unit/services/test_inventory_service.py index 56244fc1..a403a132 100644 --- a/tests/unit/services/test_inventory_service.py +++ b/tests/unit/services/test_inventory_service.py @@ -486,7 +486,7 @@ class TestInventoryService: def test_update_inventory_wrong_vendor(self, db, test_inventory, other_company): """Test updating inventory from wrong vendor raises InventoryNotFoundException.""" - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor unique_id = str(uuid.uuid4())[:8] other_vendor = Vendor( @@ -527,7 +527,7 @@ class TestInventoryService: def test_delete_inventory_wrong_vendor(self, db, test_inventory, other_company): """Test deleting inventory from wrong vendor raises InventoryNotFoundException.""" - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor unique_id = str(uuid.uuid4())[:8] other_vendor = Vendor( @@ -735,7 +735,7 @@ class TestInventoryService: self, db, test_product, other_company ): """Test _get_vendor_product raises for wrong vendor.""" - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor unique_id = str(uuid.uuid4())[:8] other_vendor = Vendor( diff --git a/tests/unit/services/test_marketplace_service.py b/tests/unit/services/test_marketplace_service.py index c69e6551..18e69107 100644 --- a/tests/unit/services/test_marketplace_service.py +++ b/tests/unit/services/test_marketplace_service.py @@ -158,7 +158,7 @@ class TestMarketplaceImportJobService: self, db, test_marketplace_import_job, other_user, other_company ): """Test getting import job for wrong vendor.""" - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor # Create another vendor unique_id = str(uuid.uuid4())[:8] @@ -241,7 +241,7 @@ class TestMarketplaceImportJobService: def test_get_import_jobs_empty(self, db, test_user, other_user, other_company): """Test getting import jobs when none exist.""" - from models.database.vendor import Vendor + from app.modules.tenancy.models import Vendor # Create a vendor with no jobs unique_id = str(uuid.uuid4())[:8] diff --git a/tests/unit/services/test_module_service.py b/tests/unit/services/test_module_service.py index 9907bc4a..db0d6ab8 100644 --- a/tests/unit/services/test_module_service.py +++ b/tests/unit/services/test_module_service.py @@ -19,7 +19,7 @@ from app.modules.registry import ( validate_module_dependencies, ) from app.modules.service import ModuleService -from models.database.admin_menu_config import FrontendType +from app.modules.enums import FrontendType @pytest.mark.unit diff --git a/tests/unit/services/test_team_service.py b/tests/unit/services/test_team_service.py index 342eae82..03cb0ea9 100644 --- a/tests/unit/services/test_team_service.py +++ b/tests/unit/services/test_team_service.py @@ -17,7 +17,7 @@ import pytest from app.exceptions import ValidationException from app.modules.tenancy.services.team_service import TeamService, team_service -from models.database.vendor import Role, VendorUser +from app.modules.tenancy.models import Role, VendorUser @pytest.mark.unit diff --git a/tests/unit/services/test_usage_service.py b/tests/unit/services/test_usage_service.py index e32d6918..21d95fce 100644 --- a/tests/unit/services/test_usage_service.py +++ b/tests/unit/services/test_usage_service.py @@ -6,7 +6,7 @@ import pytest from app.modules.analytics.services.usage_service import UsageService, usage_service 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 @pytest.mark.unit @@ -285,7 +285,7 @@ def test_vendor_with_products(db, test_vendor_with_subscription, marketplace_pro @pytest.fixture def test_vendor_with_team(db, test_vendor_with_subscription, test_user, other_user): """Create vendor with team members (owner + team member = 2).""" - from models.database.vendor import VendorUserType + from app.modules.tenancy.models import VendorUserType # Add owner owner = VendorUser( diff --git a/tests/unit/services/test_vendor_email_settings_service.py b/tests/unit/services/test_vendor_email_settings_service.py index 96f69b0c..53f2f396 100644 --- a/tests/unit/services/test_vendor_email_settings_service.py +++ b/tests/unit/services/test_vendor_email_settings_service.py @@ -11,7 +11,8 @@ from app.exceptions import ( ValidationException, ) from app.modules.cms.services.vendor_email_settings_service import VendorEmailSettingsService -from models.database import VendorEmailSettings, TierCode +from app.modules.messaging.models import VendorEmailSettings +from app.modules.billing.models import TierCode # ============================================================================= diff --git a/tests/unit/services/test_vendor_service.py b/tests/unit/services/test_vendor_service.py index 1bcd42d9..64d2f4d1 100644 --- a/tests/unit/services/test_vendor_service.py +++ b/tests/unit/services/test_vendor_service.py @@ -15,10 +15,10 @@ from app.modules.tenancy.exceptions import ( from app.modules.marketplace.exceptions import MarketplaceProductNotFoundException from app.modules.catalog.exceptions import ProductAlreadyExistsException from app.modules.tenancy.services.vendor_service import VendorService -from models.database.company import Company -from models.database.vendor import Vendor +from app.modules.tenancy.models import Company +from app.modules.tenancy.models import Vendor from app.modules.catalog.schemas import ProductCreate -from models.schema.vendor import VendorCreate +from app.modules.tenancy.schemas.vendor import VendorCreate @pytest.fixture @@ -582,7 +582,7 @@ class TestVendorServicePermissions: def test_can_update_vendor_non_owner(self, db, other_company, test_vendor): """Test non-owner cannot update vendor.""" - from models.database.user import User + from app.modules.tenancy.models import User vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first() other_user = db.query(User).filter(User.id == other_company.owner_user_id).first() @@ -598,7 +598,7 @@ class TestVendorServicePermissions: def test_is_vendor_owner_false(self, db, other_company, test_vendor): """Test _is_vendor_owner returns False for non-owner.""" - from models.database.user import User + from app.modules.tenancy.models import User vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first() other_user = db.query(User).filter(User.id == other_company.owner_user_id).first() @@ -643,7 +643,7 @@ class TestVendorServiceUpdate: from pydantic import BaseModel from app.modules.tenancy.exceptions import InsufficientPermissionsException - from models.database.user import User + from app.modules.tenancy.models import User class VendorUpdate(BaseModel): name: str | None = None @@ -695,7 +695,7 @@ class TestVendorServiceUpdate: ): """Test marketplace settings update fails for unauthorized user.""" from app.modules.tenancy.exceptions import InsufficientPermissionsException - from models.database.user import User + from app.modules.tenancy.models import User other_user = db.query(User).filter(User.id == other_company.owner_user_id).first() marketplace_config = {"letzshop_csv_url_fr": "https://example.com/fr.csv"}