diff --git a/.architecture-rules/module.yaml b/.architecture-rules/module.yaml index 5d4cf3f8..52db3c92 100644 --- a/.architecture-rules/module.yaml +++ b/.architecture-rules/module.yaml @@ -761,3 +761,96 @@ module_rules: file_pattern: "main.py" validates: - "module_locales mount BEFORE module_static mount" + + # ========================================================================= + # Cross-Module Boundary Rules + # ========================================================================= + + - id: "MOD-025" + name: "Modules must NOT import models from other modules" + severity: "error" + description: | + Modules must access data from other modules through their SERVICE layer, + never by importing and querying their models directly. + + This is the "services over models" principle: if module A needs data + from module B, it MUST call module B's service methods. + + WRONG (direct model import): + # app/modules/orders/services/order_service.py + from app.modules.catalog.models import Product # FORBIDDEN + + class OrderService: + def get_order_details(self, db, order_id): + product = db.query(Product).filter_by(id=pid).first() + + RIGHT (service call): + # app/modules/orders/services/order_service.py + from app.modules.catalog.services import product_service + + class OrderService: + def get_order_details(self, db, order_id): + product = product_service.get_product_by_id(db, pid) + + ALSO RIGHT (provider protocol for core→optional): + # app/modules/core/services/stats_aggregator.py + from app.modules.contracts.metrics import MetricsProviderProtocol + # Discover providers through registry, no direct imports + + EXCEPTIONS: + - Test fixtures may create models from other modules for setup + - TYPE_CHECKING imports for type hints are allowed + - Tenancy models (User, Store, Merchant, Platform) may be imported + as type hints in route signatures where FastAPI requires it, + but queries must go through tenancy services + + WHY THIS MATTERS: + - Encapsulation: Modules own their data access patterns + - Refactoring: Module B can change its schema without breaking A + - Testability: Mock services, not database queries + - Consistency: Clear API boundaries between modules + - Decoupling: Modules can evolve independently + pattern: + file_pattern: "app/modules/*/services/**/*.py" + anti_patterns: + - "from app\\.modules\\.(?!)\\.models import" + exceptions: + - "TYPE_CHECKING" + - "tests/" + + - id: "MOD-026" + name: "Cross-module data access must use service methods" + severity: "error" + description: | + When a module needs data from another module, it must use that + module's public service API. Each module should expose service + methods for common data access patterns. + + Service methods a module should expose: + - get_{entity}_by_id(db, id) -> Entity or None + - list_{entities}(db, filters) -> list[Entity] + - get_{entity}_count(db, filters) -> int + - search_{entities}(db, query, filters) -> list[Entity] + + WRONG (direct query across module boundary): + # In orders module + count = db.query(func.count(Product.id)).scalar() + + RIGHT (call catalog service): + # In orders module + count = product_service.get_product_count(db, store_id=store_id) + + This applies to: + - Simple lookups (get by ID) + - List/search queries + - Aggregation queries (count, sum) + - Join queries (should be decomposed into service calls) + + WHY THIS MATTERS: + - Single source of truth for data access logic + - Easier to add caching, validation, or access control + - Clear contract between modules + - Simpler testing with service mocks + pattern: + file_pattern: "app/modules/*/services/**/*.py" + check: "cross_module_service_usage" diff --git a/app/modules/analytics/locales/de.json b/app/modules/analytics/locales/de.json index 05f13301..9ffefefe 100644 --- a/app/modules/analytics/locales/de.json +++ b/app/modules/analytics/locales/de.json @@ -16,5 +16,13 @@ }, "menu": { "analytics": "Analytik" + }, + "permissions": { + "view": "Analytik anzeigen", + "view_desc": "Zugriff auf Analytik-Dashboards und Berichte", + "export": "Analytik exportieren", + "export_desc": "Analytikdaten und Berichte exportieren", + "manage_dashboards": "Dashboards verwalten", + "manage_dashboards_desc": "Analytik-Dashboards erstellen und konfigurieren" } } diff --git a/app/modules/analytics/locales/en.json b/app/modules/analytics/locales/en.json index 8dd4a61f..8857aeaf 100644 --- a/app/modules/analytics/locales/en.json +++ b/app/modules/analytics/locales/en.json @@ -14,6 +14,14 @@ "loading": "Loading analytics...", "error_loading": "Failed to load analytics data" }, + "permissions": { + "view": "View Analytics", + "view_desc": "Access analytics dashboards and reports", + "export": "Export Analytics", + "export_desc": "Export analytics data and reports", + "manage_dashboards": "Manage Dashboards", + "manage_dashboards_desc": "Create and configure analytics dashboards" + }, "menu": { "analytics": "Analytics" } diff --git a/app/modules/analytics/locales/fr.json b/app/modules/analytics/locales/fr.json index f494786e..905b2ee6 100644 --- a/app/modules/analytics/locales/fr.json +++ b/app/modules/analytics/locales/fr.json @@ -16,5 +16,13 @@ }, "menu": { "analytics": "Analytique" + }, + "permissions": { + "view": "Voir l'analytique", + "view_desc": "Accéder aux tableaux de bord et rapports analytiques", + "export": "Exporter l'analytique", + "export_desc": "Exporter les données et rapports analytiques", + "manage_dashboards": "Gérer les tableaux de bord", + "manage_dashboards_desc": "Créer et configurer les tableaux de bord analytiques" } } diff --git a/app/modules/analytics/locales/lb.json b/app/modules/analytics/locales/lb.json index 4518081a..809fedfa 100644 --- a/app/modules/analytics/locales/lb.json +++ b/app/modules/analytics/locales/lb.json @@ -16,5 +16,13 @@ }, "menu": { "analytics": "Analytik" + }, + "permissions": { + "view": "Analytik kucken", + "view_desc": "Zougang zu Analytik-Dashboards a Berichter", + "export": "Analytik exportéieren", + "export_desc": "Analytikdaten a Berichter exportéieren", + "manage_dashboards": "Dashboards verwalten", + "manage_dashboards_desc": "Analytik-Dashboards erstellen a konfiguréieren" } } diff --git a/app/modules/billing/locales/de.json b/app/modules/billing/locales/de.json index c0036fb2..17ce3f63 100644 --- a/app/modules/billing/locales/de.json +++ b/app/modules/billing/locales/de.json @@ -134,5 +134,17 @@ "invoices": "Rechnungen", "account_settings": "Kontoeinstellungen", "billing": "Abrechnung" + }, + "permissions": { + "view_tiers": "Tarife anzeigen", + "view_tiers_desc": "Details der Abonnement-Tarife anzeigen", + "manage_tiers": "Tarife verwalten", + "manage_tiers_desc": "Abonnement-Tarife erstellen und konfigurieren", + "view_subscriptions": "Abonnements anzeigen", + "view_subscriptions_desc": "Abonnementdetails anzeigen", + "manage_subscriptions": "Abonnements verwalten", + "manage_subscriptions_desc": "Abonnements und Abrechnung verwalten", + "view_invoices": "Rechnungen anzeigen", + "view_invoices_desc": "Rechnungen und Abrechnungsverlauf anzeigen" } } diff --git a/app/modules/billing/locales/en.json b/app/modules/billing/locales/en.json index c1f7b52b..f256c9fa 100644 --- a/app/modules/billing/locales/en.json +++ b/app/modules/billing/locales/en.json @@ -81,6 +81,18 @@ "current": "Current Plan", "recommended": "Recommended" }, + "permissions": { + "view_tiers": "View Tiers", + "view_tiers_desc": "View subscription tier details", + "manage_tiers": "Manage Tiers", + "manage_tiers_desc": "Create and configure subscription tiers", + "view_subscriptions": "View Subscriptions", + "view_subscriptions_desc": "View store subscription details", + "manage_subscriptions": "Manage Subscriptions", + "manage_subscriptions_desc": "Manage store subscriptions and billing", + "view_invoices": "View Invoices", + "view_invoices_desc": "View billing invoices and history" + }, "messages": { "subscription_updated": "Subscription updated successfully", "tier_created": "Tier created successfully", diff --git a/app/modules/billing/locales/fr.json b/app/modules/billing/locales/fr.json index da3df541..1d2e13cb 100644 --- a/app/modules/billing/locales/fr.json +++ b/app/modules/billing/locales/fr.json @@ -134,5 +134,17 @@ "invoices": "Factures", "account_settings": "Paramètres du compte", "billing": "Facturation" + }, + "permissions": { + "view_tiers": "Voir les niveaux", + "view_tiers_desc": "Voir les détails des niveaux d'abonnement", + "manage_tiers": "Gérer les niveaux", + "manage_tiers_desc": "Créer et configurer les niveaux d'abonnement", + "view_subscriptions": "Voir les abonnements", + "view_subscriptions_desc": "Voir les détails des abonnements", + "manage_subscriptions": "Gérer les abonnements", + "manage_subscriptions_desc": "Gérer les abonnements et la facturation", + "view_invoices": "Voir les factures", + "view_invoices_desc": "Voir les factures et l'historique de facturation" } } diff --git a/app/modules/billing/locales/lb.json b/app/modules/billing/locales/lb.json index 2a7042c0..02a747da 100644 --- a/app/modules/billing/locales/lb.json +++ b/app/modules/billing/locales/lb.json @@ -134,5 +134,17 @@ "invoices": "Rechnungen", "account_settings": "Kont-Astellungen", "billing": "Ofrechnung" + }, + "permissions": { + "view_tiers": "Tariffer kucken", + "view_tiers_desc": "Detailer vun den Abonnement-Tariffer kucken", + "manage_tiers": "Tariffer verwalten", + "manage_tiers_desc": "Abonnement-Tariffer erstellen a konfiguréieren", + "view_subscriptions": "Abonnementer kucken", + "view_subscriptions_desc": "Abonnementdetailer kucken", + "manage_subscriptions": "Abonnementer verwalten", + "manage_subscriptions_desc": "Abonnementer an Ofrechnung verwalten", + "view_invoices": "Rechnunge kucken", + "view_invoices_desc": "Rechnungen an Ofrechnungsverlaf kucken" } } diff --git a/app/modules/cart/locales/de.json b/app/modules/cart/locales/de.json index 0bedd9fa..895c8b06 100644 --- a/app/modules/cart/locales/de.json +++ b/app/modules/cart/locales/de.json @@ -1,42 +1,48 @@ { - "title": "Warenkorb", - "description": "Warenkorbverwaltung für Kunden", - "cart": { - "title": "Ihr Warenkorb", - "empty": "Ihr Warenkorb ist leer", - "empty_subtitle": "Fügen Sie Artikel hinzu, um einzukaufen", - "continue_shopping": "Weiter einkaufen", - "proceed_to_checkout": "Zur Kasse" - }, - "item": { - "product": "Produkt", - "quantity": "Menge", - "price": "Preis", - "total": "Gesamt", - "remove": "Entfernen", - "update": "Aktualisieren" - }, - "summary": { - "title": "Bestellübersicht", - "subtotal": "Zwischensumme", - "shipping": "Versand", - "estimated_shipping": "Wird an der Kasse berechnet", - "tax": "MwSt.", - "total": "Gesamtsumme" - }, - "validation": { - "invalid_quantity": "Ungültige Menge", - "min_quantity": "Mindestmenge ist {min}", - "max_quantity": "Höchstmenge ist {max}", - "insufficient_inventory": "Nur {available} verfügbar" - }, - "messages": { - "item_added": "Artikel zum Warenkorb hinzugefügt", - "item_updated": "Warenkorb aktualisiert", - "item_removed": "Artikel aus dem Warenkorb entfernt", - "cart_cleared": "Warenkorb geleert", - "product_not_available": "Produkt nicht verfügbar", - "error_adding": "Fehler beim Hinzufügen zum Warenkorb", - "error_updating": "Fehler beim Aktualisieren des Warenkorbs" - } + "title": "Warenkorb", + "description": "Warenkorbverwaltung für Kunden", + "cart": { + "title": "Ihr Warenkorb", + "empty": "Ihr Warenkorb ist leer", + "empty_subtitle": "Fügen Sie Artikel hinzu, um einzukaufen", + "continue_shopping": "Weiter einkaufen", + "proceed_to_checkout": "Zur Kasse" + }, + "item": { + "product": "Produkt", + "quantity": "Menge", + "price": "Preis", + "total": "Gesamt", + "remove": "Entfernen", + "update": "Aktualisieren" + }, + "summary": { + "title": "Bestellübersicht", + "subtotal": "Zwischensumme", + "shipping": "Versand", + "estimated_shipping": "Wird an der Kasse berechnet", + "tax": "MwSt.", + "total": "Gesamtsumme" + }, + "validation": { + "invalid_quantity": "Ungültige Menge", + "min_quantity": "Mindestmenge ist {min}", + "max_quantity": "Höchstmenge ist {max}", + "insufficient_inventory": "Nur {available} verfügbar" + }, + "messages": { + "item_added": "Artikel zum Warenkorb hinzugefügt", + "item_updated": "Warenkorb aktualisiert", + "item_removed": "Artikel aus dem Warenkorb entfernt", + "cart_cleared": "Warenkorb geleert", + "product_not_available": "Produkt nicht verfügbar", + "error_adding": "Fehler beim Hinzufügen zum Warenkorb", + "error_updating": "Fehler beim Aktualisieren des Warenkorbs" + }, + "permissions": { + "view": "Warenkörbe anzeigen", + "view_desc": "Warenkörbe der Kunden anzeigen", + "manage": "Warenkörbe verwalten", + "manage_desc": "Warenkörbe der Kunden bearbeiten und verwalten" + } } diff --git a/app/modules/cart/locales/en.json b/app/modules/cart/locales/en.json index 2d92dbd9..d77292b2 100644 --- a/app/modules/cart/locales/en.json +++ b/app/modules/cart/locales/en.json @@ -30,6 +30,12 @@ "max_quantity": "Maximum quantity is {max}", "insufficient_inventory": "Only {available} available" }, + "permissions": { + "view": "View Carts", + "view_desc": "View customer shopping carts", + "manage": "Manage Carts", + "manage_desc": "Modify and manage customer carts" + }, "messages": { "item_added": "Item added to cart", "item_updated": "Cart updated", diff --git a/app/modules/cart/locales/fr.json b/app/modules/cart/locales/fr.json index f5741d7c..682eae58 100644 --- a/app/modules/cart/locales/fr.json +++ b/app/modules/cart/locales/fr.json @@ -1,42 +1,48 @@ { - "title": "Panier", - "description": "Gestion du panier pour les clients", - "cart": { - "title": "Votre panier", - "empty": "Votre panier est vide", - "empty_subtitle": "Ajoutez des articles pour commencer vos achats", - "continue_shopping": "Continuer mes achats", - "proceed_to_checkout": "Passer à la caisse" - }, - "item": { - "product": "Produit", - "quantity": "Quantité", - "price": "Prix", - "total": "Total", - "remove": "Supprimer", - "update": "Mettre à jour" - }, - "summary": { - "title": "Récapitulatif de commande", - "subtotal": "Sous-total", - "shipping": "Livraison", - "estimated_shipping": "Calculé à la caisse", - "tax": "TVA", - "total": "Total" - }, - "validation": { - "invalid_quantity": "Quantité invalide", - "min_quantity": "Quantité minimum: {min}", - "max_quantity": "Quantité maximum: {max}", - "insufficient_inventory": "Seulement {available} disponible(s)" - }, - "messages": { - "item_added": "Article ajouté au panier", - "item_updated": "Panier mis à jour", - "item_removed": "Article supprimé du panier", - "cart_cleared": "Panier vidé", - "product_not_available": "Produit non disponible", - "error_adding": "Erreur lors de l'ajout au panier", - "error_updating": "Erreur lors de la mise à jour du panier" - } + "title": "Panier", + "description": "Gestion du panier pour les clients", + "cart": { + "title": "Votre panier", + "empty": "Votre panier est vide", + "empty_subtitle": "Ajoutez des articles pour commencer vos achats", + "continue_shopping": "Continuer mes achats", + "proceed_to_checkout": "Passer à la caisse" + }, + "item": { + "product": "Produit", + "quantity": "Quantité", + "price": "Prix", + "total": "Total", + "remove": "Supprimer", + "update": "Mettre à jour" + }, + "summary": { + "title": "Récapitulatif de commande", + "subtotal": "Sous-total", + "shipping": "Livraison", + "estimated_shipping": "Calculé à la caisse", + "tax": "TVA", + "total": "Total" + }, + "validation": { + "invalid_quantity": "Quantité invalide", + "min_quantity": "Quantité minimum: {min}", + "max_quantity": "Quantité maximum: {max}", + "insufficient_inventory": "Seulement {available} disponible(s)" + }, + "messages": { + "item_added": "Article ajouté au panier", + "item_updated": "Panier mis à jour", + "item_removed": "Article supprimé du panier", + "cart_cleared": "Panier vidé", + "product_not_available": "Produit non disponible", + "error_adding": "Erreur lors de l'ajout au panier", + "error_updating": "Erreur lors de la mise à jour du panier" + }, + "permissions": { + "view": "Voir les paniers", + "view_desc": "Voir les paniers des clients", + "manage": "Gérer les paniers", + "manage_desc": "Modifier et gérer les paniers des clients" + } } diff --git a/app/modules/cart/locales/lb.json b/app/modules/cart/locales/lb.json index 1d635117..c9fa3d81 100644 --- a/app/modules/cart/locales/lb.json +++ b/app/modules/cart/locales/lb.json @@ -1,42 +1,48 @@ { - "title": "Akafskuerf", - "description": "Kuerfverwaltung fir Clienten", - "cart": { - "title": "Äre Kuerf", - "empty": "Äre Kuerf ass eidel", - "empty_subtitle": "Setzt Artikelen derbäi fir anzekafen", - "continue_shopping": "Weider akafen", - "proceed_to_checkout": "Zur Keess" - }, - "item": { - "product": "Produkt", - "quantity": "Unzuel", - "price": "Präis", - "total": "Gesamt", - "remove": "Ewechhuelen", - "update": "Aktualiséieren" - }, - "summary": { - "title": "Bestelliwwersiicht", - "subtotal": "Zwëschesumm", - "shipping": "Liwwerung", - "estimated_shipping": "Gëtt bei der Keess berechent", - "tax": "MwSt.", - "total": "Gesamtsumm" - }, - "validation": { - "invalid_quantity": "Ongëlteg Unzuel", - "min_quantity": "Mindestunzuel ass {min}", - "max_quantity": "Héichstunzuel ass {max}", - "insufficient_inventory": "Nëmmen {available} verfügbar" - }, - "messages": { - "item_added": "Artikel an de Kuerf gesat", - "item_updated": "Kuerf aktualiséiert", - "item_removed": "Artikel aus dem Kuerf ewechgeholl", - "cart_cleared": "Kuerf eidel gemaach", - "product_not_available": "Produkt net verfügbar", - "error_adding": "Feeler beim Derbäisetzen an de Kuerf", - "error_updating": "Feeler beim Aktualiséiere vum Kuerf" - } + "title": "Akafskuerf", + "description": "Kuerfverwaltung fir Clienten", + "cart": { + "title": "Äre Kuerf", + "empty": "Äre Kuerf ass eidel", + "empty_subtitle": "Setzt Artikelen derbäi fir anzekafen", + "continue_shopping": "Weider akafen", + "proceed_to_checkout": "Zur Keess" + }, + "item": { + "product": "Produkt", + "quantity": "Unzuel", + "price": "Präis", + "total": "Gesamt", + "remove": "Ewechhuelen", + "update": "Aktualiséieren" + }, + "summary": { + "title": "Bestelliwwersiicht", + "subtotal": "Zwëschesumm", + "shipping": "Liwwerung", + "estimated_shipping": "Gëtt bei der Keess berechent", + "tax": "MwSt.", + "total": "Gesamtsumm" + }, + "validation": { + "invalid_quantity": "Ongëlteg Unzuel", + "min_quantity": "Mindestunzuel ass {min}", + "max_quantity": "Héichstunzuel ass {max}", + "insufficient_inventory": "Nëmmen {available} verfügbar" + }, + "messages": { + "item_added": "Artikel an de Kuerf gesat", + "item_updated": "Kuerf aktualiséiert", + "item_removed": "Artikel aus dem Kuerf ewechgeholl", + "cart_cleared": "Kuerf eidel gemaach", + "product_not_available": "Produkt net verfügbar", + "error_adding": "Feeler beim Derbäisetzen an de Kuerf", + "error_updating": "Feeler beim Aktualiséiere vum Kuerf" + }, + "permissions": { + "view": "Kuerf kucken", + "view_desc": "Clientekuerf kucken", + "manage": "Kuerf verwalten", + "manage_desc": "Clientekuerf änneren a verwalten" + } } diff --git a/app/modules/catalog/locales/de.json b/app/modules/catalog/locales/de.json index 9089d8ea..fa287749 100644 --- a/app/modules/catalog/locales/de.json +++ b/app/modules/catalog/locales/de.json @@ -75,5 +75,19 @@ "menu": { "products_inventory": "Produkte & Inventar", "all_products": "Alle Produkte" + }, + "permissions": { + "products_view": "Produkte anzeigen", + "products_view_desc": "Produktkatalog anzeigen", + "products_create": "Produkte erstellen", + "products_create_desc": "Neue Produkte zum Katalog hinzufügen", + "products_edit": "Produkte bearbeiten", + "products_edit_desc": "Bestehende Produktdetails ändern", + "products_delete": "Produkte löschen", + "products_delete_desc": "Produkte aus dem Katalog entfernen", + "products_import": "Produkte importieren", + "products_import_desc": "Massenimport von Produkten", + "products_export": "Produkte exportieren", + "products_export_desc": "Produktdaten exportieren" } } diff --git a/app/modules/catalog/locales/en.json b/app/modules/catalog/locales/en.json index 14e55524..8aa11642 100644 --- a/app/modules/catalog/locales/en.json +++ b/app/modules/catalog/locales/en.json @@ -46,6 +46,20 @@ "sort_name_az": "Name: A-Z", "sort_name_za": "Name: Z-A" }, + "permissions": { + "products_view": "View Products", + "products_view_desc": "View the product catalog", + "products_create": "Create Products", + "products_create_desc": "Add new products to the catalog", + "products_edit": "Edit Products", + "products_edit_desc": "Modify existing product details", + "products_delete": "Delete Products", + "products_delete_desc": "Remove products from the catalog", + "products_import": "Import Products", + "products_import_desc": "Bulk import products from files", + "products_export": "Export Products", + "products_export_desc": "Export product data to files" + }, "messages": { "product_deleted_successfully": "Product deleted successfully", "product_created_successfully": "Product created successfully", diff --git a/app/modules/catalog/locales/fr.json b/app/modules/catalog/locales/fr.json index 6913a0ee..22c49d18 100644 --- a/app/modules/catalog/locales/fr.json +++ b/app/modules/catalog/locales/fr.json @@ -75,5 +75,19 @@ "menu": { "products_inventory": "Produits et Inventaire", "all_products": "Tous les produits" + }, + "permissions": { + "products_view": "Voir les produits", + "products_view_desc": "Voir le catalogue de produits", + "products_create": "Créer des produits", + "products_create_desc": "Ajouter de nouveaux produits au catalogue", + "products_edit": "Modifier les produits", + "products_edit_desc": "Modifier les détails des produits existants", + "products_delete": "Supprimer les produits", + "products_delete_desc": "Retirer des produits du catalogue", + "products_import": "Importer les produits", + "products_import_desc": "Importation en masse de produits", + "products_export": "Exporter les produits", + "products_export_desc": "Exporter les données produits" } } diff --git a/app/modules/catalog/locales/lb.json b/app/modules/catalog/locales/lb.json index 5587543c..faf1cbd9 100644 --- a/app/modules/catalog/locales/lb.json +++ b/app/modules/catalog/locales/lb.json @@ -75,5 +75,19 @@ "menu": { "products_inventory": "Produkter & Inventar", "all_products": "All Produkter" + }, + "permissions": { + "products_view": "Produiten kucken", + "products_view_desc": "Produitkatalog kucken", + "products_create": "Produiten erstellen", + "products_create_desc": "Nei Produiten an de Katalog derbäisetzen", + "products_edit": "Produiten änneren", + "products_edit_desc": "Bestoend Produitdetailer änneren", + "products_delete": "Produiten läschen", + "products_delete_desc": "Produiten aus dem Katalog ewechhuelen", + "products_import": "Produiten importéieren", + "products_import_desc": "Massenimport vu Produiten", + "products_export": "Produiten exportéieren", + "products_export_desc": "Produitdaten exportéieren" } } diff --git a/app/modules/checkout/locales/de.json b/app/modules/checkout/locales/de.json index 521b8350..53771251 100644 --- a/app/modules/checkout/locales/de.json +++ b/app/modules/checkout/locales/de.json @@ -1,20 +1,26 @@ { - "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", - "thank_you": "Vielen Dank für Ihre Bestellung", - "order_confirmation": "Bestellbestätigung" - } + "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", + "thank_you": "Vielen Dank für Ihre Bestellung", + "order_confirmation": "Bestellbestätigung" + }, + "permissions": { + "view_settings": "Kassen-Einstellungen anzeigen", + "view_settings_desc": "Kassen-Konfiguration anzeigen", + "manage_settings": "Kassen-Einstellungen verwalten", + "manage_settings_desc": "Kassenprozess und Optionen konfigurieren" + } } diff --git a/app/modules/checkout/locales/en.json b/app/modules/checkout/locales/en.json index 899d2274..67ac421d 100644 --- a/app/modules/checkout/locales/en.json +++ b/app/modules/checkout/locales/en.json @@ -1,4 +1,10 @@ { + "permissions": { + "view_settings": "View Checkout Settings", + "view_settings_desc": "View checkout configuration", + "manage_settings": "Manage Checkout Settings", + "manage_settings_desc": "Configure checkout process and options" + }, "storefront": { "welcome": "Welcome to our store", "browse_products": "Browse Products", diff --git a/app/modules/checkout/locales/fr.json b/app/modules/checkout/locales/fr.json index 43baeac5..540e1c96 100644 --- a/app/modules/checkout/locales/fr.json +++ b/app/modules/checkout/locales/fr.json @@ -1,20 +1,26 @@ { - "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", - "thank_you": "Merci pour votre commande", - "order_confirmation": "Confirmation de commande" - } + "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", + "thank_you": "Merci pour votre commande", + "order_confirmation": "Confirmation de commande" + }, + "permissions": { + "view_settings": "Voir les paramètres de paiement", + "view_settings_desc": "Voir la configuration du processus de paiement", + "manage_settings": "Gérer les paramètres de paiement", + "manage_settings_desc": "Configurer le processus et les options de paiement" + } } diff --git a/app/modules/checkout/locales/lb.json b/app/modules/checkout/locales/lb.json index e0cec9ff..df2206f9 100644 --- a/app/modules/checkout/locales/lb.json +++ b/app/modules/checkout/locales/lb.json @@ -1,20 +1,26 @@ { - "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", - "thank_you": "Merci fir Är Bestellung", - "order_confirmation": "Bestellungsbestätegung" - } + "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", + "thank_you": "Merci fir Är Bestellung", + "order_confirmation": "Bestellungsbestätegung" + }, + "permissions": { + "view_settings": "Keess-Astellunge kucken", + "view_settings_desc": "Keess-Konfiguratioun kucken", + "manage_settings": "Keess-Astellunge verwalten", + "manage_settings_desc": "Keessprozess an Optiounen konfiguréieren" + } } diff --git a/app/modules/cms/locales/de.json b/app/modules/cms/locales/de.json index db20b29a..408fa77a 100644 --- a/app/modules/cms/locales/de.json +++ b/app/modules/cms/locales/de.json @@ -234,5 +234,17 @@ "content_pages": "Inhaltsseiten", "store_themes": "Shop-Themes", "media_library": "Mediathek" + }, + "permissions": { + "view_pages": "Seiten anzeigen", + "view_pages_desc": "Inhaltsseiten anzeigen", + "manage_pages": "Seiten verwalten", + "manage_pages_desc": "Inhaltsseiten erstellen, bearbeiten und löschen", + "view_media": "Medien anzeigen", + "view_media_desc": "Medienbibliothek durchsuchen", + "manage_media": "Medien verwalten", + "manage_media_desc": "Mediendateien hochladen, bearbeiten und löschen", + "manage_themes": "Themes verwalten", + "manage_themes_desc": "Shop-Themes konfigurieren und anpassen" } } diff --git a/app/modules/cms/locales/en.json b/app/modules/cms/locales/en.json index 2d66f128..8ca2b470 100644 --- a/app/modules/cms/locales/en.json +++ b/app/modules/cms/locales/en.json @@ -200,6 +200,18 @@ "cta_final_note": "No credit card required. Setup in 5 minutes. Full Professional features during trial." } }, + "permissions": { + "view_pages": "View Pages", + "view_pages_desc": "View content pages", + "manage_pages": "Manage Pages", + "manage_pages_desc": "Create, edit, and delete content pages", + "view_media": "View Media", + "view_media_desc": "Browse the media library", + "manage_media": "Manage Media", + "manage_media_desc": "Upload, edit, and delete media files", + "manage_themes": "Manage Themes", + "manage_themes_desc": "Configure and customize store themes" + }, "messages": { "failed_to_delete_page": "Failed to delete page: {error}", "media_updated_successfully": "Media updated successfully", diff --git a/app/modules/cms/locales/fr.json b/app/modules/cms/locales/fr.json index 190ad54e..e98416de 100644 --- a/app/modules/cms/locales/fr.json +++ b/app/modules/cms/locales/fr.json @@ -234,5 +234,17 @@ "content_pages": "Pages de contenu", "store_themes": "Thèmes du magasin", "media_library": "Médiathèque" + }, + "permissions": { + "view_pages": "Voir les pages", + "view_pages_desc": "Voir les pages de contenu", + "manage_pages": "Gérer les pages", + "manage_pages_desc": "Créer, modifier et supprimer les pages de contenu", + "view_media": "Voir les médias", + "view_media_desc": "Parcourir la bibliothèque de médias", + "manage_media": "Gérer les médias", + "manage_media_desc": "Télécharger, modifier et supprimer les fichiers médias", + "manage_themes": "Gérer les thèmes", + "manage_themes_desc": "Configurer et personnaliser les thèmes" } } diff --git a/app/modules/cms/locales/lb.json b/app/modules/cms/locales/lb.json index 01b77b72..ba4cf5e0 100644 --- a/app/modules/cms/locales/lb.json +++ b/app/modules/cms/locales/lb.json @@ -234,5 +234,17 @@ "content_pages": "Inhaltsäiten", "store_themes": "Buttek-Themen", "media_library": "Mediathéik" + }, + "permissions": { + "view_pages": "Säiten kucken", + "view_pages_desc": "Inhaltssäiten kucken", + "manage_pages": "Säiten verwalten", + "manage_pages_desc": "Inhaltssäiten erstellen, änneren a läschen", + "view_media": "Medien kucken", + "view_media_desc": "Mediebibliothéik duerchsichen", + "manage_media": "Medien verwalten", + "manage_media_desc": "Mediefichieren eroplueden, änneren a läschen", + "manage_themes": "Themes verwalten", + "manage_themes_desc": "Buttek-Themes konfiguréieren an upassen" } } diff --git a/app/modules/core/locales/de.json b/app/modules/core/locales/de.json index 048886c2..f357a56f 100644 --- a/app/modules/core/locales/de.json +++ b/app/modules/core/locales/de.json @@ -76,5 +76,17 @@ "account_settings": "Kontoeinstellungen", "profile": "Profil", "settings": "Einstellungen" + }, + "permissions": { + "dashboard_view": "Dashboard anzeigen", + "dashboard_view_desc": "Zugriff auf das Dashboard und die Übersicht", + "settings_view": "Einstellungen anzeigen", + "settings_view_desc": "Shop-Konfiguration anzeigen", + "settings_edit": "Einstellungen bearbeiten", + "settings_edit_desc": "Shop-Konfiguration ändern", + "settings_theme": "Theme verwalten", + "settings_theme_desc": "Shop-Theme und Erscheinungsbild anpassen", + "settings_domains": "Domains verwalten", + "settings_domains_desc": "Benutzerdefinierte Domains konfigurieren" } } diff --git a/app/modules/core/locales/en.json b/app/modules/core/locales/en.json index 53b993e7..44b4521b 100644 --- a/app/modules/core/locales/en.json +++ b/app/modules/core/locales/en.json @@ -64,6 +64,18 @@ "save_profile": "Save Profile", "profile_updated": "Profile updated successfully" }, + "permissions": { + "dashboard_view": "View Dashboard", + "dashboard_view_desc": "Access the store dashboard and overview", + "settings_view": "View Settings", + "settings_view_desc": "View store settings and configuration", + "settings_edit": "Edit Settings", + "settings_edit_desc": "Modify store settings and configuration", + "settings_theme": "Manage Theme", + "settings_theme_desc": "Customize the store theme and appearance", + "settings_domains": "Manage Domains", + "settings_domains_desc": "Configure custom domains for the store" + }, "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 index cafa6e65..62156d14 100644 --- a/app/modules/core/locales/fr.json +++ b/app/modules/core/locales/fr.json @@ -76,5 +76,17 @@ "account_settings": "Paramètres du compte", "profile": "Profil", "settings": "Paramètres" + }, + "permissions": { + "dashboard_view": "Voir le tableau de bord", + "dashboard_view_desc": "Accéder au tableau de bord et à la vue d'ensemble", + "settings_view": "Voir les paramètres", + "settings_view_desc": "Voir la configuration du magasin", + "settings_edit": "Modifier les paramètres", + "settings_edit_desc": "Modifier la configuration du magasin", + "settings_theme": "Gérer le thème", + "settings_theme_desc": "Personnaliser le thème et l'apparence du magasin", + "settings_domains": "Gérer les domaines", + "settings_domains_desc": "Configurer les domaines personnalisés" } } diff --git a/app/modules/core/locales/lb.json b/app/modules/core/locales/lb.json index 9beccdda..ff56ed40 100644 --- a/app/modules/core/locales/lb.json +++ b/app/modules/core/locales/lb.json @@ -76,5 +76,17 @@ "account_settings": "Kont-Astellungen", "profile": "Profil", "settings": "Astellungen" + }, + "permissions": { + "dashboard_view": "Dashboard kucken", + "dashboard_view_desc": "Zougang zum Dashboard an der Iwwersiicht", + "settings_view": "Astellungen kucken", + "settings_view_desc": "Buttek-Konfiguratioun kucken", + "settings_edit": "Astellungen änneren", + "settings_edit_desc": "Buttek-Konfiguratioun änneren", + "settings_theme": "Theme verwalten", + "settings_theme_desc": "Buttek-Theme an Ausgesinn upassen", + "settings_domains": "Domänen verwalten", + "settings_domains_desc": "Personaliséiert Domäne konfiguréieren" } } diff --git a/app/modules/core/static/shared/js/store-selector.js b/app/modules/core/static/shared/js/store-selector.js index c2f6341b..4b5eba1d 100644 --- a/app/modules/core/static/shared/js/store-selector.js +++ b/app/modules/core/static/shared/js/store-selector.js @@ -166,8 +166,9 @@ function initStoreSelector(selectElement, options = {}) { } try { + const sep = config.apiEndpoint.includes('?') ? '&' : '?'; const response = await apiClient.get( - `${config.apiEndpoint}?search=${encodeURIComponent(query)}&limit=${config.maxOptions}` + `${config.apiEndpoint}${sep}search=${encodeURIComponent(query)}&limit=${config.maxOptions}` ); const stores = (response.stores || []).map(v => ({ diff --git a/app/modules/customers/locales/de.json b/app/modules/customers/locales/de.json index 0a7b591b..f3e0629a 100644 --- a/app/modules/customers/locales/de.json +++ b/app/modules/customers/locales/de.json @@ -37,5 +37,15 @@ "customers_section": "Kunden", "customers": "Kunden", "all_customers": "Alle Kunden" + }, + "permissions": { + "customers_view": "Kunden anzeigen", + "customers_view_desc": "Kundenliste und Details anzeigen", + "customers_edit": "Kunden bearbeiten", + "customers_edit_desc": "Kundeninformationen ändern", + "customers_delete": "Kunden löschen", + "customers_delete_desc": "Kundendatensätze entfernen", + "customers_export": "Kunden exportieren", + "customers_export_desc": "Kundendaten exportieren" } } diff --git a/app/modules/customers/locales/en.json b/app/modules/customers/locales/en.json index 6e000ba3..76f7fa49 100644 --- a/app/modules/customers/locales/en.json +++ b/app/modules/customers/locales/en.json @@ -18,6 +18,16 @@ "no_customers": "No customers found", "search_customers": "Search customers..." }, + "permissions": { + "customers_view": "View Customers", + "customers_view_desc": "View customer list and details", + "customers_edit": "Edit Customers", + "customers_edit_desc": "Modify customer information", + "customers_delete": "Delete Customers", + "customers_delete_desc": "Remove customer records", + "customers_export": "Export Customers", + "customers_export_desc": "Export customer data to files" + }, "messages": { "failed_to_toggle_customer_status": "Failed to toggle customer status", "failed_to_load_customer_details": "Failed to load customer details", diff --git a/app/modules/customers/locales/fr.json b/app/modules/customers/locales/fr.json index 7908eaa1..cd2dedb5 100644 --- a/app/modules/customers/locales/fr.json +++ b/app/modules/customers/locales/fr.json @@ -37,5 +37,15 @@ "customers_section": "Clients", "customers": "Clients", "all_customers": "Tous les clients" + }, + "permissions": { + "customers_view": "Voir les clients", + "customers_view_desc": "Voir la liste et les détails des clients", + "customers_edit": "Modifier les clients", + "customers_edit_desc": "Modifier les informations client", + "customers_delete": "Supprimer les clients", + "customers_delete_desc": "Supprimer les fiches clients", + "customers_export": "Exporter les clients", + "customers_export_desc": "Exporter les données clients" } } diff --git a/app/modules/customers/locales/lb.json b/app/modules/customers/locales/lb.json index 3cbfa711..9bce5bd6 100644 --- a/app/modules/customers/locales/lb.json +++ b/app/modules/customers/locales/lb.json @@ -37,5 +37,15 @@ "customers_section": "Clienten", "customers": "Clienten", "all_customers": "All Clienten" + }, + "permissions": { + "customers_view": "Clienten kucken", + "customers_view_desc": "Clientelëscht an Detailer kucken", + "customers_edit": "Clienten änneren", + "customers_edit_desc": "Clienteninformatiounen änneren", + "customers_delete": "Clienten läschen", + "customers_delete_desc": "Clientedossieren ewechhuelen", + "customers_export": "Clienten exportéieren", + "customers_export_desc": "Clientedaten exportéieren" } } diff --git a/app/modules/inventory/locales/de.json b/app/modules/inventory/locales/de.json index b35cf9af..4f61bba1 100644 --- a/app/modules/inventory/locales/de.json +++ b/app/modules/inventory/locales/de.json @@ -42,5 +42,13 @@ "products_inventory": "Produkte & Inventar", "products": "Produkte", "inventory": "Inventar" + }, + "permissions": { + "stock_view": "Inventar anzeigen", + "stock_view_desc": "Bestände und Inventardaten anzeigen", + "stock_edit": "Inventar bearbeiten", + "stock_edit_desc": "Bestände und Mengen anpassen", + "stock_transfer": "Bestand transferieren", + "stock_transfer_desc": "Bestand zwischen Standorten transferieren" } } diff --git a/app/modules/inventory/locales/en.json b/app/modules/inventory/locales/en.json index 31a00a9a..2e291400 100644 --- a/app/modules/inventory/locales/en.json +++ b/app/modules/inventory/locales/en.json @@ -12,6 +12,14 @@ "low_stock_alert": "Low Stock Alert", "out_of_stock_alert": "Out of Stock Alert" }, + "permissions": { + "stock_view": "View Inventory", + "stock_view_desc": "View stock levels and inventory data", + "stock_edit": "Edit Inventory", + "stock_edit_desc": "Adjust stock levels and quantities", + "stock_transfer": "Transfer Stock", + "stock_transfer_desc": "Transfer stock between locations" + }, "messages": { "stock_adjusted_successfully": "Stock adjusted successfully", "quantity_set_successfully": "Quantity set successfully", diff --git a/app/modules/inventory/locales/fr.json b/app/modules/inventory/locales/fr.json index c526b45a..7d5a6e3b 100644 --- a/app/modules/inventory/locales/fr.json +++ b/app/modules/inventory/locales/fr.json @@ -42,5 +42,13 @@ "products_inventory": "Produits et Inventaire", "products": "Produits", "inventory": "Inventaire" + }, + "permissions": { + "stock_view": "Voir l'inventaire", + "stock_view_desc": "Voir les niveaux de stock et les données d'inventaire", + "stock_edit": "Modifier l'inventaire", + "stock_edit_desc": "Ajuster les niveaux de stock et les quantités", + "stock_transfer": "Transférer le stock", + "stock_transfer_desc": "Transférer le stock entre les emplacements" } } diff --git a/app/modules/inventory/locales/lb.json b/app/modules/inventory/locales/lb.json index 94b0942e..406c4d58 100644 --- a/app/modules/inventory/locales/lb.json +++ b/app/modules/inventory/locales/lb.json @@ -42,5 +42,13 @@ "products_inventory": "Produkter & Inventar", "products": "Produkter", "inventory": "Inventar" + }, + "permissions": { + "stock_view": "Inventar kucken", + "stock_view_desc": "Bestandsniveauen an Inventardaten kucken", + "stock_edit": "Inventar änneren", + "stock_edit_desc": "Bestandsniveauen a Quantitéiten upassen", + "stock_transfer": "Bestand transferéieren", + "stock_transfer_desc": "Bestand tëschent Standuerten transferéieren" } } diff --git a/app/modules/loyalty/locales/de.json b/app/modules/loyalty/locales/de.json index 0d6c10d2..22e17c3e 100644 --- a/app/modules/loyalty/locales/de.json +++ b/app/modules/loyalty/locales/de.json @@ -80,5 +80,15 @@ "statistics": "Statistiken", "overview": "Übersicht", "settings": "Einstellungen" + }, + "permissions": { + "view_programs": "Programme anzeigen", + "view_programs_desc": "Treueprogramme und Details anzeigen", + "manage_programs": "Programme verwalten", + "manage_programs_desc": "Treueprogramme erstellen und konfigurieren", + "view_rewards": "Prämien anzeigen", + "view_rewards_desc": "Prämien und Einlösungen anzeigen", + "manage_rewards": "Prämien verwalten", + "manage_rewards_desc": "Treueprämien erstellen und verwalten" } } diff --git a/app/modules/loyalty/locales/en.json b/app/modules/loyalty/locales/en.json index b288b40b..2e590845 100644 --- a/app/modules/loyalty/locales/en.json +++ b/app/modules/loyalty/locales/en.json @@ -69,6 +69,16 @@ "pin_locked": "PIN locked due to too many failed attempts" } }, + "permissions": { + "view_programs": "View Programs", + "view_programs_desc": "View loyalty programs and details", + "manage_programs": "Manage Programs", + "manage_programs_desc": "Create and configure loyalty programs", + "view_rewards": "View Rewards", + "view_rewards_desc": "View loyalty rewards and redemptions", + "manage_rewards": "Manage Rewards", + "manage_rewards_desc": "Create and manage loyalty rewards" + }, "menu": { "loyalty": "Loyalty", "loyalty_programs": "Loyalty Programs", diff --git a/app/modules/loyalty/locales/fr.json b/app/modules/loyalty/locales/fr.json index 0f7de3f3..f013f2de 100644 --- a/app/modules/loyalty/locales/fr.json +++ b/app/modules/loyalty/locales/fr.json @@ -80,5 +80,15 @@ "statistics": "Statistiques", "overview": "Aperçu", "settings": "Paramètres" + }, + "permissions": { + "view_programs": "Voir les programmes", + "view_programs_desc": "Voir les programmes de fidélité et leurs détails", + "manage_programs": "Gérer les programmes", + "manage_programs_desc": "Créer et configurer les programmes de fidélité", + "view_rewards": "Voir les récompenses", + "view_rewards_desc": "Voir les récompenses et les échanges", + "manage_rewards": "Gérer les récompenses", + "manage_rewards_desc": "Créer et gérer les récompenses de fidélité" } } diff --git a/app/modules/loyalty/locales/lb.json b/app/modules/loyalty/locales/lb.json index cf3d56a9..fe6dfe8d 100644 --- a/app/modules/loyalty/locales/lb.json +++ b/app/modules/loyalty/locales/lb.json @@ -80,5 +80,15 @@ "statistics": "Statistiken", "overview": "Iwwersiicht", "settings": "Astellungen" + }, + "permissions": { + "view_programs": "Programmer kucken", + "view_programs_desc": "Treiheet-Programmer an Detailer kucken", + "manage_programs": "Programmer verwalten", + "manage_programs_desc": "Treiheet-Programmer erstellen a konfiguréieren", + "view_rewards": "Beloununge kucken", + "view_rewards_desc": "Belounungen an Aléisunge kucken", + "manage_rewards": "Beloununge verwalten", + "manage_rewards_desc": "Treiheet-Belounungen erstellen a verwalten" } } diff --git a/app/modules/marketplace/locales/de.json b/app/modules/marketplace/locales/de.json index 71f63f12..aacb5713 100644 --- a/app/modules/marketplace/locales/de.json +++ b/app/modules/marketplace/locales/de.json @@ -1,87 +1,95 @@ { - "menu": { - "marketplace": "Marktplatz", - "letzshop": "Letzshop", - "products_inventory": "Produkte & Inventar", - "marketplace_import": "Marktplatz Import", - "sales_orders": "Verkäufe & Bestellungen", - "letzshop_orders": "Letzshop Bestellungen" - }, - "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" + "menu": { + "marketplace": "Marktplatz", + "letzshop": "Letzshop", + "products_inventory": "Produkte & Inventar", + "marketplace_import": "Marktplatz Import", + "sales_orders": "Verkäufe & Bestellungen", + "letzshop_orders": "Letzshop Bestellungen" + }, + "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" + } + }, + "messages": { + "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" + }, + "features": { + "letzshop_sync": { + "name": "Lëtzshop-Synchronisation", + "description": "Produkte mit dem Lëtzshop-Marktplatz synchronisieren" + }, + "api_access": { + "name": "API-Zugang", + "description": "Zugang zur Plattform-API" + }, + "webhooks": { + "name": "Webhooks", + "description": "Echtzeit-Ereignisbenachrichtigungen über Webhooks" + }, + "custom_integrations": { + "name": "Eigene Integrationen", + "description": "Eigene Integrationen mit der Plattform erstellen" + } + }, + "permissions": { + "view_integration": "Integration anzeigen", + "view_integration_desc": "Marktplatz-Integrationseinstellungen anzeigen", + "manage_integration": "Integration verwalten", + "manage_integration_desc": "Marktplatz-Integration konfigurieren", + "sync_products": "Produkte synchronisieren", + "sync_products_desc": "Produkte mit dem Marktplatz synchronisieren" } - }, - "messages": { - "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" - }, - "features": { - "letzshop_sync": { - "name": "Lëtzshop-Synchronisation", - "description": "Produkte mit dem Lëtzshop-Marktplatz synchronisieren" - }, - "api_access": { - "name": "API-Zugang", - "description": "Zugang zur Plattform-API" - }, - "webhooks": { - "name": "Webhooks", - "description": "Echtzeit-Ereignisbenachrichtigungen über Webhooks" - }, - "custom_integrations": { - "name": "Eigene Integrationen", - "description": "Eigene Integrationen mit der Plattform erstellen" - } - } } diff --git a/app/modules/marketplace/locales/en.json b/app/modules/marketplace/locales/en.json index af46d483..82f40051 100644 --- a/app/modules/marketplace/locales/en.json +++ b/app/modules/marketplace/locales/en.json @@ -7,6 +7,14 @@ "sales_orders": "Sales & Orders", "letzshop_orders": "Letzshop Orders" }, + "permissions": { + "view_integration": "View Integration", + "view_integration_desc": "View marketplace integration settings", + "manage_integration": "Manage Integration", + "manage_integration_desc": "Configure marketplace integration", + "sync_products": "Sync Products", + "sync_products_desc": "Synchronize products with marketplace" + }, "marketplace": { "title": "Marketplace", "import": "Import", diff --git a/app/modules/marketplace/locales/fr.json b/app/modules/marketplace/locales/fr.json index f826f135..fafe02be 100644 --- a/app/modules/marketplace/locales/fr.json +++ b/app/modules/marketplace/locales/fr.json @@ -1,87 +1,95 @@ { - "menu": { - "marketplace": "Marketplace", - "letzshop": "Letzshop", - "products_inventory": "Produits et Inventaire", - "marketplace_import": "Import Marketplace", - "sales_orders": "Ventes et Commandes", - "letzshop_orders": "Commandes Letzshop" - }, - "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" + "menu": { + "marketplace": "Marketplace", + "letzshop": "Letzshop", + "products_inventory": "Produits et Inventaire", + "marketplace_import": "Import Marketplace", + "sales_orders": "Ventes et Commandes", + "letzshop_orders": "Commandes Letzshop" + }, + "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" + } + }, + "messages": { + "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" + }, + "features": { + "letzshop_sync": { + "name": "Synchronisation Lëtzshop", + "description": "Synchroniser les produits avec la marketplace Lëtzshop" + }, + "api_access": { + "name": "Accès API", + "description": "Accès à l'API de la plateforme" + }, + "webhooks": { + "name": "Webhooks", + "description": "Notifications d'événements en temps réel via webhooks" + }, + "custom_integrations": { + "name": "Intégrations personnalisées", + "description": "Créer des intégrations personnalisées avec la plateforme" + } + }, + "permissions": { + "view_integration": "Voir l'intégration", + "view_integration_desc": "Voir les paramètres d'intégration marketplace", + "manage_integration": "Gérer l'intégration", + "manage_integration_desc": "Configurer l'intégration marketplace", + "sync_products": "Synchroniser les produits", + "sync_products_desc": "Synchroniser les produits avec le marketplace" } - }, - "messages": { - "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" - }, - "features": { - "letzshop_sync": { - "name": "Synchronisation Lëtzshop", - "description": "Synchroniser les produits avec la marketplace Lëtzshop" - }, - "api_access": { - "name": "Accès API", - "description": "Accès à l'API de la plateforme" - }, - "webhooks": { - "name": "Webhooks", - "description": "Notifications d'événements en temps réel via webhooks" - }, - "custom_integrations": { - "name": "Intégrations personnalisées", - "description": "Créer des intégrations personnalisées avec la plateforme" - } - } } diff --git a/app/modules/marketplace/locales/lb.json b/app/modules/marketplace/locales/lb.json index 27e224c5..9c22742b 100644 --- a/app/modules/marketplace/locales/lb.json +++ b/app/modules/marketplace/locales/lb.json @@ -1,87 +1,95 @@ { - "menu": { - "marketplace": "Marchéplaz", - "letzshop": "Letzshop", - "products_inventory": "Produkter & Inventar", - "marketplace_import": "Marchéplaz Import", - "sales_orders": "Verkaf & Bestellungen", - "letzshop_orders": "Letzshop Bestellungen" - }, - "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" + "menu": { + "marketplace": "Marchéplaz", + "letzshop": "Letzshop", + "products_inventory": "Produkter & Inventar", + "marketplace_import": "Marchéplaz Import", + "sales_orders": "Verkaf & Bestellungen", + "letzshop_orders": "Letzshop Bestellungen" + }, + "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" + } + }, + "messages": { + "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" + }, + "features": { + "letzshop_sync": { + "name": "Lëtzshop-Synchronisatioun", + "description": "Produkter mam Lëtzshop-Marktplaz synchroniséieren" + }, + "api_access": { + "name": "API-Zougang", + "description": "Zougang zur Plattform-API" + }, + "webhooks": { + "name": "Webhooks", + "description": "Echtzäit-Evenement-Benoriichtegungen iwwer Webhooks" + }, + "custom_integrations": { + "name": "Eegen Integratiounen", + "description": "Eegen Integratiounen mat der Plattform erstellen" + } + }, + "permissions": { + "view_integration": "Integratioun kucken", + "view_integration_desc": "Marché-Integratiounsastellungen kucken", + "manage_integration": "Integratioun verwalten", + "manage_integration_desc": "Marché-Integratioun konfiguréieren", + "sync_products": "Produiten synchroniséieren", + "sync_products_desc": "Produiten mam Marché synchroniséieren" } - }, - "messages": { - "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" - }, - "features": { - "letzshop_sync": { - "name": "Lëtzshop-Synchronisatioun", - "description": "Produkter mam Lëtzshop-Marktplaz synchroniséieren" - }, - "api_access": { - "name": "API-Zougang", - "description": "Zougang zur Plattform-API" - }, - "webhooks": { - "name": "Webhooks", - "description": "Echtzäit-Evenement-Benoriichtegungen iwwer Webhooks" - }, - "custom_integrations": { - "name": "Eegen Integratiounen", - "description": "Eegen Integratiounen mat der Plattform erstellen" - } - } } diff --git a/app/modules/messaging/locales/de.json b/app/modules/messaging/locales/de.json index 25d59448..b111cfe1 100644 --- a/app/modules/messaging/locales/de.json +++ b/app/modules/messaging/locales/de.json @@ -60,5 +60,13 @@ "messages": "Nachrichten", "notifications": "Benachrichtigungen", "email_templates": "E-Mail-Vorlagen" + }, + "permissions": { + "view_messages": "Nachrichten anzeigen", + "view_messages_desc": "Nachrichten und Konversationen anzeigen", + "send_messages": "Nachrichten senden", + "send_messages_desc": "Nachrichten an Kunden senden", + "manage_templates": "Vorlagen verwalten", + "manage_templates_desc": "Nachrichtenvorlagen erstellen und bearbeiten" } } diff --git a/app/modules/messaging/locales/en.json b/app/modules/messaging/locales/en.json index bf7da4de..5b2e674a 100644 --- a/app/modules/messaging/locales/en.json +++ b/app/modules/messaging/locales/en.json @@ -10,6 +10,14 @@ "import_complete": "Import Complete", "import_failed": "Import Failed" }, + "permissions": { + "view_messages": "View Messages", + "view_messages_desc": "View messages and conversations", + "send_messages": "Send Messages", + "send_messages_desc": "Send messages to customers", + "manage_templates": "Manage Templates", + "manage_templates_desc": "Create and edit message templates" + }, "messages": { "failed_to_load_template": "Failed to load template", "template_saved_successfully": "Template saved successfully", diff --git a/app/modules/messaging/locales/fr.json b/app/modules/messaging/locales/fr.json index 99273ede..9cb441af 100644 --- a/app/modules/messaging/locales/fr.json +++ b/app/modules/messaging/locales/fr.json @@ -60,5 +60,13 @@ "messages": "Messages", "notifications": "Notifications", "email_templates": "Modèles d'e-mail" + }, + "permissions": { + "view_messages": "Voir les messages", + "view_messages_desc": "Voir les messages et conversations", + "send_messages": "Envoyer des messages", + "send_messages_desc": "Envoyer des messages aux clients", + "manage_templates": "Gérer les modèles", + "manage_templates_desc": "Créer et modifier les modèles de messages" } } diff --git a/app/modules/messaging/locales/lb.json b/app/modules/messaging/locales/lb.json index a1aba88b..34b3380f 100644 --- a/app/modules/messaging/locales/lb.json +++ b/app/modules/messaging/locales/lb.json @@ -60,5 +60,13 @@ "messages": "Messagen", "notifications": "Notifikatiounen", "email_templates": "E-Mail-Virlagen" + }, + "permissions": { + "view_messages": "Messagen kucken", + "view_messages_desc": "Messagen a Conversatiounen kucken", + "send_messages": "Messagen schécken", + "send_messages_desc": "Messagen u Clienten schécken", + "manage_templates": "Schablounen verwalten", + "manage_templates_desc": "Messageschablounen erstellen an änneren" } } diff --git a/app/modules/orders/locales/de.json b/app/modules/orders/locales/de.json index aade3b60..56ab7026 100644 --- a/app/modules/orders/locales/de.json +++ b/app/modules/orders/locales/de.json @@ -79,5 +79,15 @@ "store_operations": "Shop-Betrieb", "sales_orders": "Verkäufe & Bestellungen", "orders": "Bestellungen" + }, + "permissions": { + "orders_view": "Bestellungen anzeigen", + "orders_view_desc": "Bestellliste und Details anzeigen", + "orders_edit": "Bestellungen bearbeiten", + "orders_edit_desc": "Bestellstatus und Details aktualisieren", + "orders_cancel": "Bestellungen stornieren", + "orders_cancel_desc": "Ausstehende oder in Bearbeitung befindliche Bestellungen stornieren", + "orders_refund": "Bestellungen erstatten", + "orders_refund_desc": "Rückerstattungen für Bestellungen verarbeiten" } } diff --git a/app/modules/orders/locales/en.json b/app/modules/orders/locales/en.json index 99a42f4b..ce7b5190 100644 --- a/app/modules/orders/locales/en.json +++ b/app/modules/orders/locales/en.json @@ -37,6 +37,16 @@ "set_tracking": "Set Tracking", "view_details": "View Details" }, + "permissions": { + "orders_view": "View Orders", + "orders_view_desc": "View order list and details", + "orders_edit": "Edit Orders", + "orders_edit_desc": "Update order status and details", + "orders_cancel": "Cancel Orders", + "orders_cancel_desc": "Cancel pending or processing orders", + "orders_refund": "Refund Orders", + "orders_refund_desc": "Process refunds for orders" + }, "messages": { "order_status_updated": "Order status updated", "item_shipped_successfully": "Item shipped successfully", diff --git a/app/modules/orders/locales/fr.json b/app/modules/orders/locales/fr.json index fc60c0fe..acd837c3 100644 --- a/app/modules/orders/locales/fr.json +++ b/app/modules/orders/locales/fr.json @@ -79,5 +79,15 @@ "store_operations": "Opérations du magasin", "sales_orders": "Ventes et Commandes", "orders": "Commandes" + }, + "permissions": { + "orders_view": "Voir les commandes", + "orders_view_desc": "Voir la liste et les détails des commandes", + "orders_edit": "Modifier les commandes", + "orders_edit_desc": "Mettre à jour le statut et les détails des commandes", + "orders_cancel": "Annuler les commandes", + "orders_cancel_desc": "Annuler les commandes en attente ou en traitement", + "orders_refund": "Rembourser les commandes", + "orders_refund_desc": "Traiter les remboursements des commandes" } } diff --git a/app/modules/orders/locales/lb.json b/app/modules/orders/locales/lb.json index 8ad7b19a..9d91ef67 100644 --- a/app/modules/orders/locales/lb.json +++ b/app/modules/orders/locales/lb.json @@ -79,5 +79,15 @@ "store_operations": "Buttek-Operatiounen", "sales_orders": "Verkaf & Bestellungen", "orders": "Bestellungen" + }, + "permissions": { + "orders_view": "Bestellunge kucken", + "orders_view_desc": "Bestelllëscht an Detailer kucken", + "orders_edit": "Bestellungen änneren", + "orders_edit_desc": "Bestellstatus an Detailer aktualiséieren", + "orders_cancel": "Bestellungen stornéieren", + "orders_cancel_desc": "Aussteesend oder a Veraarbechtung Bestellunge stornéieren", + "orders_refund": "Bestellungen erstatten", + "orders_refund_desc": "Réckerstattunge fir Bestellunge veraarbechten" } } diff --git a/app/modules/payments/locales/de.json b/app/modules/payments/locales/de.json index d347014a..89b8933c 100644 --- a/app/modules/payments/locales/de.json +++ b/app/modules/payments/locales/de.json @@ -1,12 +1,20 @@ { - "menu": { - "payments": "Zahlungen" - }, - "payments": { - "title": "Zahlungen" - }, - "messages": { - "payment_successful": "Zahlung erfolgreich verarbeitet", - "payment_failed": "Zahlungsverarbeitung fehlgeschlagen" - } + "menu": { + "payments": "Zahlungen" + }, + "payments": { + "title": "Zahlungen" + }, + "messages": { + "payment_successful": "Zahlung erfolgreich verarbeitet", + "payment_failed": "Zahlungsverarbeitung fehlgeschlagen" + }, + "permissions": { + "view_gateways": "Zahlungsgateways anzeigen", + "view_gateways_desc": "Konfigurationen der Zahlungsgateways anzeigen", + "manage_gateways": "Zahlungsgateways verwalten", + "manage_gateways_desc": "Zahlungsgateways konfigurieren und verwalten", + "view_transactions": "Transaktionen anzeigen", + "view_transactions_desc": "Zahlungstransaktionsverlauf anzeigen" + } } diff --git a/app/modules/payments/locales/en.json b/app/modules/payments/locales/en.json index c42a43da..24360f65 100644 --- a/app/modules/payments/locales/en.json +++ b/app/modules/payments/locales/en.json @@ -5,6 +5,14 @@ "payments": { "title": "Payments" }, + "permissions": { + "view_gateways": "View Gateways", + "view_gateways_desc": "View payment gateway configurations", + "manage_gateways": "Manage Gateways", + "manage_gateways_desc": "Configure and manage payment gateways", + "view_transactions": "View Transactions", + "view_transactions_desc": "View payment transaction history" + }, "messages": { "payment_successful": "Payment processed successfully", "payment_failed": "Payment processing failed" diff --git a/app/modules/payments/locales/fr.json b/app/modules/payments/locales/fr.json index 09e67775..cf7ce057 100644 --- a/app/modules/payments/locales/fr.json +++ b/app/modules/payments/locales/fr.json @@ -1,12 +1,20 @@ { - "menu": { - "payments": "Paiements" - }, - "payments": { - "title": "Paiements" - }, - "messages": { - "payment_successful": "Paiement traité avec succès", - "payment_failed": "Échec du traitement du paiement" - } + "menu": { + "payments": "Paiements" + }, + "payments": { + "title": "Paiements" + }, + "messages": { + "payment_successful": "Paiement traité avec succès", + "payment_failed": "Échec du traitement du paiement" + }, + "permissions": { + "view_gateways": "Voir les passerelles", + "view_gateways_desc": "Voir les configurations des passerelles de paiement", + "manage_gateways": "Gérer les passerelles", + "manage_gateways_desc": "Configurer et gérer les passerelles de paiement", + "view_transactions": "Voir les transactions", + "view_transactions_desc": "Voir l'historique des transactions de paiement" + } } diff --git a/app/modules/payments/locales/lb.json b/app/modules/payments/locales/lb.json index 77369e8c..9ed6d539 100644 --- a/app/modules/payments/locales/lb.json +++ b/app/modules/payments/locales/lb.json @@ -1,12 +1,20 @@ { - "menu": { - "payments": "Bezuelungen" - }, - "payments": { - "title": "Bezuelungen" - }, - "messages": { - "payment_successful": "Bezuelung erfollegräich veraarbecht", - "payment_failed": "Bezuelungsveraarbechtung ass feelgeschloen" - } + "menu": { + "payments": "Bezuelungen" + }, + "payments": { + "title": "Bezuelungen" + }, + "messages": { + "payment_successful": "Bezuelung erfollegräich veraarbecht", + "payment_failed": "Bezuelungsveraarbechtung ass feelgeschloen" + }, + "permissions": { + "view_gateways": "Bezuelungsgateways kucken", + "view_gateways_desc": "Konfiguratiounen vun de Bezuelungsgateways kucken", + "manage_gateways": "Bezuelungsgateways verwalten", + "manage_gateways_desc": "Bezuelungsgateways konfiguréieren a verwalten", + "view_transactions": "Transaktiounen kucken", + "view_transactions_desc": "Bezuelungstransaktiounsverlaf kucken" + } } diff --git a/app/modules/tenancy/definition.py b/app/modules/tenancy/definition.py index 07101019..dbe70d3b 100644 --- a/app/modules/tenancy/definition.py +++ b/app/modules/tenancy/definition.py @@ -87,9 +87,11 @@ tenancy_module = ModuleDefinition( "stores", "admin-users", "merchant-users", + "store-roles", ], FrontendType.STORE: [ "team", + "roles", ], FrontendType.MERCHANT: [ "stores", @@ -122,6 +124,13 @@ tenancy_module = ModuleDefinition( order=20, is_mandatory=True, ), + MenuItemDefinition( + id="store-roles", + label_key="tenancy.menu.store_roles", + icon="shield-check", + route="/admin/store-roles", + order=30, + ), ], ), MenuSectionDefinition( @@ -202,6 +211,14 @@ tenancy_module = ModuleDefinition( route="/store/{store_code}/team", order=5, ), + MenuItemDefinition( + id="roles", + label_key="tenancy.menu.roles", + icon="shield-check", + route="/store/{store_code}/team/roles", + order=10, + requires_permission="team.view", + ), ], ), ], diff --git a/app/modules/tenancy/locales/de.json b/app/modules/tenancy/locales/de.json index 2fe0af03..780b7339 100644 --- a/app/modules/tenancy/locales/de.json +++ b/app/modules/tenancy/locales/de.json @@ -112,5 +112,37 @@ "name": "Audit-Protokoll", "description": "Alle Benutzeraktionen und Änderungen nachverfolgen" } + }, + "permissions": { + "category": { + "dashboard": "Dashboard", + "products": "Produkte", + "stock": "Inventar", + "orders": "Bestellungen", + "customers": "Kunden", + "marketing": "Marketing", + "reports": "Berichte", + "settings": "Einstellungen", + "team": "Team", + "imports": "Importe", + "general": "Allgemein", + "analytics": "Analytik", + "billing": "Abrechnung", + "cart": "Warenkorb", + "checkout": "Kasse", + "cms": "Inhalte", + "loyalty": "Treue", + "marketplace": "Marktplatz", + "messaging": "Nachrichten", + "payments": "Zahlungen" + }, + "team_view": "Team anzeigen", + "team_view_desc": "Teammitglieder und ihre Rollen anzeigen", + "team_invite": "Mitglieder einladen", + "team_invite_desc": "Neue Mitglieder ins Team einladen", + "team_edit": "Mitglieder bearbeiten", + "team_edit_desc": "Rollen und Berechtigungen der Mitglieder bearbeiten", + "team_remove": "Mitglieder entfernen", + "team_remove_desc": "Mitglieder aus dem Team entfernen" } } diff --git a/app/modules/tenancy/locales/en.json b/app/modules/tenancy/locales/en.json index 133b4002..572204d8 100644 --- a/app/modules/tenancy/locales/en.json +++ b/app/modules/tenancy/locales/en.json @@ -97,6 +97,38 @@ "show_all_menu_items": "This will show all menu items. Continue?", "hide_all_menu_items": "This will hide all menu items (except mandatory ones). You can then enable the ones you want. Continue?" }, + "permissions": { + "category": { + "dashboard": "Dashboard", + "products": "Products", + "stock": "Inventory", + "orders": "Orders", + "customers": "Customers", + "marketing": "Marketing", + "reports": "Reports", + "settings": "Settings", + "team": "Team", + "imports": "Imports", + "general": "General", + "analytics": "Analytics", + "billing": "Billing", + "cart": "Cart", + "checkout": "Checkout", + "cms": "Content", + "loyalty": "Loyalty", + "marketplace": "Marketplace", + "messaging": "Messaging", + "payments": "Payments" + }, + "team_view": "View Team", + "team_view_desc": "View team members and their roles", + "team_invite": "Invite Members", + "team_invite_desc": "Invite new members to the store team", + "team_edit": "Edit Members", + "team_edit_desc": "Edit team member roles and permissions", + "team_remove": "Remove Members", + "team_remove_desc": "Remove members from the store team" + }, "features": { "team_members": { "name": "Team Members", diff --git a/app/modules/tenancy/locales/fr.json b/app/modules/tenancy/locales/fr.json index 5b1de444..36216b10 100644 --- a/app/modules/tenancy/locales/fr.json +++ b/app/modules/tenancy/locales/fr.json @@ -112,5 +112,37 @@ "name": "Journal d'audit", "description": "Suivre toutes les actions et modifications" } + }, + "permissions": { + "category": { + "dashboard": "Tableau de bord", + "products": "Produits", + "stock": "Inventaire", + "orders": "Commandes", + "customers": "Clients", + "marketing": "Marketing", + "reports": "Rapports", + "settings": "Paramètres", + "team": "Équipe", + "imports": "Importations", + "general": "Général", + "analytics": "Analytique", + "billing": "Facturation", + "cart": "Panier", + "checkout": "Paiement", + "cms": "Contenu", + "loyalty": "Fidélité", + "marketplace": "Marketplace", + "messaging": "Messagerie", + "payments": "Paiements" + }, + "team_view": "Voir l'équipe", + "team_view_desc": "Voir les membres de l'équipe et leurs rôles", + "team_invite": "Inviter des membres", + "team_invite_desc": "Inviter de nouveaux membres dans l'équipe", + "team_edit": "Modifier les membres", + "team_edit_desc": "Modifier les rôles et permissions des membres", + "team_remove": "Supprimer des membres", + "team_remove_desc": "Retirer des membres de l'équipe" } } diff --git a/app/modules/tenancy/locales/lb.json b/app/modules/tenancy/locales/lb.json index 7b6e46e2..b05134e7 100644 --- a/app/modules/tenancy/locales/lb.json +++ b/app/modules/tenancy/locales/lb.json @@ -112,5 +112,37 @@ "name": "Audit-Protokoll", "description": "All Benotzeraktiounen an Ännerungen nospueren" } + }, + "permissions": { + "category": { + "dashboard": "Dashboard", + "products": "Produiten", + "stock": "Inventar", + "orders": "Bestellungen", + "customers": "Clienten", + "marketing": "Marketing", + "reports": "Berichter", + "settings": "Astellungen", + "team": "Team", + "imports": "Importatiounen", + "general": "Allgemeng", + "analytics": "Analytik", + "billing": "Ofrechnung", + "cart": "Kuerf", + "checkout": "Keess", + "cms": "Inhalter", + "loyalty": "Treiheet", + "marketplace": "Marché", + "messaging": "Messagen", + "payments": "Bezuelungen" + }, + "team_view": "Team kucken", + "team_view_desc": "Team-Memberen an hir Rollen kucken", + "team_invite": "Memberen invitéieren", + "team_invite_desc": "Nei Memberen an d'Team invitéieren", + "team_edit": "Memberen änneren", + "team_edit_desc": "Rollen a Rechter vun de Memberen änneren", + "team_remove": "Memberen ewechhuelen", + "team_remove_desc": "Memberen aus dem Team ewechhuelen" } } diff --git a/app/modules/tenancy/routes/api/admin.py b/app/modules/tenancy/routes/api/admin.py index cf47514b..3d500660 100644 --- a/app/modules/tenancy/routes/api/admin.py +++ b/app/modules/tenancy/routes/api/admin.py @@ -26,6 +26,7 @@ from .admin_modules import router as admin_modules_router from .admin_platform_users import admin_platform_users_router from .admin_platforms import admin_platforms_router from .admin_store_domains import admin_store_domains_router +from .admin_store_roles import admin_store_roles_router from .admin_stores import admin_stores_router from .admin_users import admin_users_router @@ -39,6 +40,7 @@ admin_router.include_router(admin_merchants_router, tags=["admin-merchants"]) admin_router.include_router(admin_platforms_router, tags=["admin-platforms"]) admin_router.include_router(admin_stores_router, tags=["admin-stores"]) admin_router.include_router(admin_store_domains_router, tags=["admin-store-domains"]) +admin_router.include_router(admin_store_roles_router, tags=["admin-store-roles"]) admin_router.include_router(admin_merchant_domains_router, tags=["admin-merchant-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_store_roles.py b/app/modules/tenancy/routes/api/admin_store_roles.py new file mode 100644 index 00000000..57572382 --- /dev/null +++ b/app/modules/tenancy/routes/api/admin_store_roles.py @@ -0,0 +1,181 @@ +# app/modules/tenancy/routes/api/admin_store_roles.py +""" +Admin store role management endpoints. + +Allows super admins and platform admins to manage roles for any store +they have access to. Platform admins are scoped to stores within their +assigned platforms. + +Endpoints: + GET /admin/store-roles — List roles for a store + GET /admin/store-roles/permissions/catalog — Permission catalog + POST /admin/store-roles — Create a role + PUT /admin/store-roles/{role_id} — Update a role + DELETE /admin/store-roles/{role_id} — Delete a role +""" + +import logging + +from fastapi import APIRouter, Depends, Query, Request +from sqlalchemy.orm import Session + +from app.api.deps import get_current_admin_api +from app.core.database import get_db +from app.modules.tenancy.schemas.team import ( + PermissionCatalogResponse, + RoleCreate, + RoleListResponse, + RoleResponse, + RoleUpdate, +) +from app.modules.tenancy.services.permission_discovery_service import ( + permission_discovery_service, +) +from app.modules.tenancy.services.store_team_service import store_team_service +from app.utils.i18n import translate +from models.schema.auth import UserContext + +admin_store_roles_router = APIRouter(prefix="/store-roles") +logger = logging.getLogger(__name__) + + +@admin_store_roles_router.get( + "/permissions/catalog", response_model=PermissionCatalogResponse +) +def admin_get_permission_catalog( + request: Request, + current_admin: UserContext = Depends(get_current_admin_api), +): + """ + Get the full permission catalog grouped by category. + + Available to all admin users. Returns all permission definitions + with labels and descriptions for the role editor UI. + """ + categories = permission_discovery_service.get_permissions_by_category() + lang = current_admin.preferred_language or getattr( + request.state, "language", "en" + ) + + def _t(key: str) -> str: + """Translate key, falling back to readable version.""" + translated = translate(key, language=lang) + if translated == key: + parts = key.split(".") + return parts[-1].replace("_", " ").title() + return translated + + return PermissionCatalogResponse( + categories=[ + { + "id": cat.id, + "label": _t(cat.label_key), + "permissions": [ + { + "id": p.id, + "label": _t(p.label_key), + "description": _t(p.description_key), + "is_owner_only": p.is_owner_only, + } + for p in cat.permissions + ], + } + for cat in categories + ] + ) + + +@admin_store_roles_router.get("", response_model=RoleListResponse) +def admin_list_store_roles( + store_id: int = Query(..., description="Store ID to list roles for"), + db: Session = Depends(get_db), + current_admin: UserContext = Depends(get_current_admin_api), +): + """ + List all roles for a store. + + Platform admins can only access stores within their assigned platforms. + Super admins can access any store. + """ + store_team_service.validate_admin_store_access(db, current_admin, store_id) + + roles = store_team_service.get_store_roles(db=db, store_id=store_id) + db.commit() # Commit in case default roles were created + + return RoleListResponse(roles=roles, total=len(roles)) + + +@admin_store_roles_router.post("", response_model=RoleResponse, status_code=201) +def admin_create_store_role( + role_data: RoleCreate, + store_id: int = Query(..., description="Store ID to create role for"), + db: Session = Depends(get_db), + current_admin: UserContext = Depends(get_current_admin_api), +): + """ + Create a custom role for a store. + + Platform admins can only manage stores within their assigned platforms. + """ + store_team_service.validate_admin_store_access(db, current_admin, store_id) + + role = store_team_service.create_custom_role( + db=db, + store_id=store_id, + name=role_data.name, + permissions=role_data.permissions, + actor_user_id=current_admin.id, + ) + db.commit() + return role + + +@admin_store_roles_router.put("/{role_id}", response_model=RoleResponse) +def admin_update_store_role( + role_id: int, + role_data: RoleUpdate, + store_id: int = Query(..., description="Store ID"), + db: Session = Depends(get_db), + current_admin: UserContext = Depends(get_current_admin_api), +): + """ + Update a role's name and/or permissions. + + Platform admins can only manage stores within their assigned platforms. + """ + store_team_service.validate_admin_store_access(db, current_admin, store_id) + + role = store_team_service.update_role( + db=db, + store_id=store_id, + role_id=role_id, + name=role_data.name, + permissions=role_data.permissions, + actor_user_id=current_admin.id, + ) + db.commit() + return role + + +@admin_store_roles_router.delete("/{role_id}", status_code=204) +def admin_delete_store_role( + role_id: int, + store_id: int = Query(..., description="Store ID"), + db: Session = Depends(get_db), + current_admin: UserContext = Depends(get_current_admin_api), +): + """ + Delete a custom role. + + Preset roles cannot be deleted. Platform admins can only manage + stores within their assigned platforms. + """ + store_team_service.validate_admin_store_access(db, current_admin, store_id) + + store_team_service.delete_role( + db=db, + store_id=store_id, + role_id=role_id, + actor_user_id=current_admin.id, + ) + db.commit() diff --git a/app/modules/tenancy/routes/api/admin_stores.py b/app/modules/tenancy/routes/api/admin_stores.py index 86d5c206..319ed8f6 100644 --- a/app/modules/tenancy/routes/api/admin_stores.py +++ b/app/modules/tenancy/routes/api/admin_stores.py @@ -86,6 +86,7 @@ def get_all_stores_admin( search: str | None = Query(None, description="Search by name or store code"), is_active: bool | None = Query(None), is_verified: bool | None = Query(None), + merchant_id: int | None = Query(None, description="Filter by merchant ID"), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): @@ -97,6 +98,7 @@ def get_all_stores_admin( search=search, is_active=is_active, is_verified=is_verified, + merchant_id=merchant_id, ) return StoreListResponse(stores=stores, total=total, skip=skip, limit=limit) diff --git a/app/modules/tenancy/routes/api/store_team.py b/app/modules/tenancy/routes/api/store_team.py index 336c35de..6cf37e8f 100644 --- a/app/modules/tenancy/routes/api/store_team.py +++ b/app/modules/tenancy/routes/api/store_team.py @@ -28,6 +28,7 @@ from app.modules.tenancy.schemas.team import ( InvitationAccept, InvitationAcceptResponse, InvitationResponse, + PermissionCatalogResponse, RoleCreate, RoleListResponse, RoleResponse, @@ -42,7 +43,11 @@ from app.modules.tenancy.schemas.team import ( # Permission IDs are now defined in module definition.py files # and discovered by PermissionDiscoveryService +from app.modules.tenancy.services.permission_discovery_service import ( + permission_discovery_service, +) from app.modules.tenancy.services.store_team_service import store_team_service +from app.utils.i18n import translate from models.schema.auth import UserContext store_team_router = APIRouter(prefix="/team") @@ -480,6 +485,57 @@ def delete_role( # ============================================================================ +@store_team_router.get( + "/permissions/catalog", response_model=PermissionCatalogResponse +) +def get_permission_catalog( + request: Request, + current_user: UserContext = Depends( + require_store_permission("team.view") + ), +): + """ + Get the full permission catalog grouped by category. + + **Required Permission:** `team.view` + + Returns all available permissions with labels and descriptions, + grouped by category. Used by the role editor UI for displaying + permission checkboxes with human-readable names and tooltips. + """ + categories = permission_discovery_service.get_permissions_by_category() + lang = current_user.preferred_language or getattr( + request.state, "language", "en" + ) + + def _t(key: str) -> str: + """Translate key, falling back to readable version.""" + translated = translate(key, language=lang) + if translated == key: + parts = key.split(".") + return parts[-1].replace("_", " ").title() + return translated + + return PermissionCatalogResponse( + categories=[ + { + "id": cat.id, + "label": _t(cat.label_key), + "permissions": [ + { + "id": p.id, + "label": _t(p.label_key), + "description": _t(p.description_key), + "is_owner_only": p.is_owner_only, + } + for p in cat.permissions + ], + } + for cat in categories + ] + ) + + @store_team_router.get("/me/permissions", response_model=UserPermissionsResponse) def get_my_permissions( request: Request, diff --git a/app/modules/tenancy/routes/pages/admin.py b/app/modules/tenancy/routes/pages/admin.py index 823f64d5..6177b66f 100644 --- a/app/modules/tenancy/routes/pages/admin.py +++ b/app/modules/tenancy/routes/pages/admin.py @@ -197,6 +197,34 @@ async def admin_store_domains_page( ) +# ============================================================================ +# STORE ROLES ROUTES +# ============================================================================ + + +@router.get("/store-roles", response_class=HTMLResponse, include_in_schema=False) +async def admin_store_roles_page( + request: Request, + current_user: User = Depends( + require_menu_access("store-roles", FrontendType.ADMIN) + ), + db: Session = Depends(get_db), +): + """ + Render store roles management page. + Allows admins to select a store and manage its roles and permissions. + Super admins see merchant → store cascading selection. + Platform admins see store selection scoped to their platforms. + """ + is_super_admin = current_user.role == "super_admin" + return templates.TemplateResponse( + "tenancy/admin/store-roles.html", + get_admin_context( + request, db, current_user, is_super_admin=is_super_admin + ), + ) + + # ============================================================================ # STORE THEMES ROUTES # ============================================================================ diff --git a/app/modules/tenancy/schemas/team.py b/app/modules/tenancy/schemas/team.py index f68975ae..4d28e002 100644 --- a/app/modules/tenancy/schemas/team.py +++ b/app/modules/tenancy/schemas/team.py @@ -276,6 +276,34 @@ class UserPermissionsResponse(BaseModel): role_name: str | None = None +# ============================================================================ +# Permission Catalog Schemas +# ============================================================================ + + +class PermissionCatalogItem(BaseModel): + """A single permission with its metadata for UI display.""" + + id: str + label: str + description: str + is_owner_only: bool = False + + +class PermissionCategoryResponse(BaseModel): + """A category of related permissions.""" + + id: str + label: str + permissions: list[PermissionCatalogItem] + + +class PermissionCatalogResponse(BaseModel): + """Complete permission catalog grouped by category.""" + + categories: list[PermissionCategoryResponse] + + # ============================================================================ # Error Response Schema # ============================================================================ diff --git a/app/modules/tenancy/services/admin_service.py b/app/modules/tenancy/services/admin_service.py index 869b0c28..bfc326bb 100644 --- a/app/modules/tenancy/services/admin_service.py +++ b/app/modules/tenancy/services/admin_service.py @@ -476,12 +476,17 @@ class AdminService: search: str | None = None, is_active: bool | None = None, is_verified: bool | None = None, + merchant_id: int | None = None, ) -> tuple[list[Store], int]: """Get paginated list of all stores with filtering.""" try: # Eagerly load merchant relationship to avoid N+1 queries query = db.query(Store).options(joinedload(Store.merchant)) + # Filter by merchant + if merchant_id is not None: + query = query.filter(Store.merchant_id == merchant_id) + # Apply search filter if search: search_term = f"%{search}%" @@ -501,6 +506,8 @@ class AdminService: # Get total count (without joinedload for performance) count_query = db.query(Store) + if merchant_id is not None: + count_query = count_query.filter(Store.merchant_id == merchant_id) if search: search_term = f"%{search}%" count_query = count_query.filter( diff --git a/app/modules/tenancy/services/store_team_service.py b/app/modules/tenancy/services/store_team_service.py index 26c1978a..7162e7ba 100644 --- a/app/modules/tenancy/services/store_team_service.py +++ b/app/modules/tenancy/services/store_team_service.py @@ -35,7 +35,7 @@ from app.modules.tenancy.exceptions import ( TeamMemberAlreadyExistsException, UserNotFoundException, ) -from app.modules.tenancy.models import Role, Store, StoreUser, User +from app.modules.tenancy.models import Role, Store, StorePlatform, StoreUser, User from middleware.auth import AuthManager logger = logging.getLogger(__name__) @@ -724,6 +724,59 @@ class StoreTeamService: details={"role_name": role_name, "store_id": store_id}, ) + # ======================================================================== + # Admin Access Validation + # ======================================================================== + + def validate_admin_store_access( + self, + db: Session, + user_context, + store_id: int, + ) -> Store: + """ + Verify an admin user can access the given store. + + Super admins can access any store. Platform admins can only access + stores that belong to one of their assigned platforms. + + Args: + db: Database session + user_context: UserContext of the admin user + store_id: Store ID to validate access to + + Returns: + The Store object if access is granted + + Raises: + InvalidRoleException: If store not found or admin lacks access + """ + store = db.query(Store).filter(Store.id == store_id).first() + if not store: + raise InvalidRoleException(f"Store {store_id} not found") + + # Super admins (accessible_platform_ids is None) can access all stores + platform_ids = user_context.get_accessible_platform_ids() + if platform_ids is None: + return store + + # Platform admins: store must belong to one of their platforms + store_in_platform = ( + db.query(StorePlatform) + .filter( + StorePlatform.store_id == store_id, + StorePlatform.platform_id.in_(platform_ids), + StorePlatform.is_active == True, + ) + .first() + ) + if not store_in_platform: + raise InvalidRoleException( + "You do not have access to this store's roles" + ) + + return store + # Private helper methods def _generate_invitation_token(self) -> str: diff --git a/app/modules/tenancy/static/admin/js/store-roles.js b/app/modules/tenancy/static/admin/js/store-roles.js new file mode 100644 index 00000000..cb2610e2 --- /dev/null +++ b/app/modules/tenancy/static/admin/js/store-roles.js @@ -0,0 +1,419 @@ +// noqa: js-006 - async init pattern is safe, loadData has try/catch +// static/admin/js/store-roles.js +/** + * Admin store roles management page + * + * Super admins: merchant → store cascading selection. + * Platform admins: store selection scoped to their platforms. + * + * Uses Tom Select for selection and permission catalog API for + * displaying permissions with labels and descriptions. + */ + +const storeRolesAdminLog = (window.LogConfig && window.LogConfig.createLogger) + ? window.LogConfig.createLogger('adminStoreRoles', false) + : console; + +storeRolesAdminLog.info('Loading...'); + +function adminStoreRoles() { + storeRolesAdminLog.info('adminStoreRoles() called'); + + const config = window._adminStoreRolesConfig || {}; + const isSuperAdmin = config.isSuperAdmin || false; + + return { + // Inherit base layout state + ...data(), + + // Set page identifier + currentPage: 'store-roles', + + // Selection state + isSuperAdmin, + selectedMerchant: null, + selectedStore: null, + merchantSelector: null, + storeSelector: null, + + // Role state + loading: false, + roles: [], + rolesLoading: false, + saving: false, + showRoleModal: false, + editingRole: null, + roleForm: { name: '', permissions: [] }, + permissionCategories: [], + presetRoles: ['manager', 'staff', 'support', 'viewer', 'marketing'], + + async init() { + // Guard against multiple initialization + if (window._adminStoreRolesInitialized) { + return; + } + window._adminStoreRolesInitialized = true; + + storeRolesAdminLog.info('Admin Store Roles init(), isSuperAdmin:', isSuperAdmin); + + this.$nextTick(() => { + if (isSuperAdmin) { + this.initMerchantSelector(); + } else { + this.initStoreSelector(); + } + }); + + // Load permission catalog + await this.loadPermissionCatalog(); + + // Restore saved selection + const savedStoreId = localStorage.getItem('admin_store_roles_selected_store_id'); + if (savedStoreId) { + storeRolesAdminLog.info('Restoring saved store:', savedStoreId); + setTimeout(async () => { + await this.restoreSavedStore(parseInt(savedStoreId)); + }, 300); + } + + storeRolesAdminLog.info('Admin Store Roles initialization complete'); + }, + + // ===================================================================== + // Permission Catalog + // ===================================================================== + + async loadPermissionCatalog() { + try { + const response = await apiClient.get('/admin/store-roles/permissions/catalog'); + this.permissionCategories = response.categories || []; + storeRolesAdminLog.info('Loaded permission catalog:', this.permissionCategories.length, 'categories'); + } catch (error) { + storeRolesAdminLog.warn('Failed to load permission catalog:', error); + } + }, + + // ===================================================================== + // Merchant Selector (Super Admin only) + // ===================================================================== + + initMerchantSelector() { + const el = this.$refs.merchantSelect; + if (!el) { + storeRolesAdminLog.warn('Merchant select element not found'); + return; + } + + const self = this; + + waitForTomSelect(() => { + self.merchantSelector = new TomSelect(el, { + valueField: 'id', + labelField: 'name', + searchField: ['name'], + maxOptions: 50, + placeholder: 'Search merchant by name...', + load: async function(query, callback) { + if (query.length < 2) { callback([]); return; } + try { + const response = await apiClient.get( + `/admin/merchants?search=${encodeURIComponent(query)}&limit=50` + ); + const merchants = (response.merchants || []).map(m => ({ + id: m.id, + name: m.name, + store_count: m.store_count || 0, + })); + callback(merchants); + } catch (error) { + storeRolesAdminLog.error('Merchant search failed:', error); + callback([]); + } + }, + render: { + option: function(data, escape) { + return `
+ ${escape(data.name)} + ${data.store_count} store(s) +
`; + }, + item: function(data, escape) { + return `
${escape(data.name)}
`; + }, + no_results: function() { + return '
No merchants found
'; + }, + loading: function() { + return '
Searching...
'; + } + }, + onChange: function(value) { + if (value) { + const selected = this.options[value]; + if (selected) { + self.onMerchantSelected({ + id: parseInt(value), + name: selected.name, + store_count: selected.store_count, + }); + } + } else { + self.onMerchantCleared(); + } + }, + loadThrottle: 150, + closeAfterSelect: true, + persist: true, + create: false, + }); + + storeRolesAdminLog.info('Merchant selector initialized'); + }); + }, + + async onMerchantSelected(merchant) { + storeRolesAdminLog.info('Merchant selected:', merchant.name); + this.selectedMerchant = merchant; + this.selectedStore = null; + this.roles = []; + localStorage.removeItem('admin_store_roles_selected_store_id'); + + // Destroy previous store selector and reinit with merchant filter + if (this.storeSelector) { + if (typeof this.storeSelector.destroy === 'function') { + this.storeSelector.destroy(); + } + this.storeSelector = null; + } + + // Wait for DOM update (x-show toggles the store select container) + await this.$nextTick(); + + this.initStoreSelector(merchant.id); + }, + + onMerchantCleared() { + storeRolesAdminLog.info('Merchant cleared'); + this.selectedMerchant = null; + this.selectedStore = null; + this.roles = []; + localStorage.removeItem('admin_store_roles_selected_store_id'); + + if (this.storeSelector) { + if (typeof this.storeSelector.destroy === 'function') { + this.storeSelector.destroy(); + } + this.storeSelector = null; + } + }, + + // ===================================================================== + // Store Selector + // ===================================================================== + + initStoreSelector(merchantId = null) { + const el = this.$refs.storeSelect; + if (!el) { + storeRolesAdminLog.warn('Store select element not found'); + return; + } + + const apiEndpoint = merchantId + ? `/admin/stores?merchant_id=${merchantId}` + : '/admin/stores'; + + this.storeSelector = initStoreSelector(el, { + placeholder: merchantId ? 'Select store...' : 'Search store by name or code...', + apiEndpoint: apiEndpoint, + onSelect: async (store) => { + storeRolesAdminLog.info('Store selected:', store); + this.selectedStore = store; + localStorage.setItem('admin_store_roles_selected_store_id', store.id.toString()); + await this.loadRoles(); + }, + onClear: () => { + storeRolesAdminLog.info('Store cleared'); + this.selectedStore = null; + this.roles = []; + localStorage.removeItem('admin_store_roles_selected_store_id'); + } + }); + }, + + // ===================================================================== + // Restore / Clear + // ===================================================================== + + async restoreSavedStore(storeId) { + try { + const store = await apiClient.get(`/admin/stores/${storeId}`); + if (!store) return; + + if (isSuperAdmin && store.merchant_id) { + // For super admin, restore the merchant first + try { + const merchant = await apiClient.get(`/admin/merchants/${store.merchant_id}`); + if (merchant && this.merchantSelector) { + this.merchantSelector.addOption({ + id: merchant.id, + name: merchant.name, + store_count: merchant.store_count || 0, + }); + this.merchantSelector.setValue(merchant.id, true); + this.selectedMerchant = { id: merchant.id, name: merchant.name }; + + // Wait for DOM, then init store selector and set value + await this.$nextTick(); + this.initStoreSelector(merchant.id); + setTimeout(() => { + if (this.storeSelector) { + this.storeSelector.setValue(store.id, store); + } + this.selectedStore = { id: store.id, name: store.name, store_code: store.store_code }; + this.loadRoles(); + }, 300); + } + } catch (error) { + storeRolesAdminLog.warn('Failed to restore merchant:', error); + localStorage.removeItem('admin_store_roles_selected_store_id'); + } + } else { + // Platform admin: just restore the store + if (this.storeSelector) { + this.storeSelector.setValue(store.id, store); + } + this.selectedStore = { id: store.id, name: store.name, store_code: store.store_code }; + await this.loadRoles(); + } + storeRolesAdminLog.info('Restored store:', store.name); + } catch (error) { + storeRolesAdminLog.warn('Failed to restore saved store:', error); + localStorage.removeItem('admin_store_roles_selected_store_id'); + } + }, + + clearSelection() { + if (isSuperAdmin) { + if (this.merchantSelector) { + this.merchantSelector.clear(); + } + this.selectedMerchant = null; + } + if (this.storeSelector) { + if (typeof this.storeSelector.clear === 'function') { + this.storeSelector.clear(); + } + } + this.selectedStore = null; + this.roles = []; + localStorage.removeItem('admin_store_roles_selected_store_id'); + }, + + // ===================================================================== + // Roles CRUD + // ===================================================================== + + async loadRoles() { + if (!this.selectedStore) return; + + this.rolesLoading = true; + try { + const response = await apiClient.get(`/admin/store-roles?store_id=${this.selectedStore.id}`); + this.roles = response.roles || []; + storeRolesAdminLog.info('Loaded', this.roles.length, 'roles'); + } catch (error) { + storeRolesAdminLog.error('Failed to load roles:', error); + Utils.showToast(error.message || 'Failed to load roles', 'error'); + } finally { + this.rolesLoading = false; + } + }, + + isPresetRole(name) { + return this.presetRoles.includes(name.toLowerCase()); + }, + + openCreateModal() { + this.editingRole = null; + this.roleForm = { name: '', permissions: [] }; + this.showRoleModal = true; + }, + + openEditModal(role) { + this.editingRole = role; + this.roleForm = { + name: role.name, + permissions: [...(role.permissions || [])], + }; + this.showRoleModal = true; + }, + + togglePermission(permId) { + const idx = this.roleForm.permissions.indexOf(permId); + if (idx >= 0) { + this.roleForm.permissions.splice(idx, 1); + } else { + this.roleForm.permissions.push(permId); + } + }, + + toggleCategory(category) { + const perms = category.permissions || []; + const permIds = perms.map(p => p.id); + const allSelected = permIds.every(id => this.roleForm.permissions.includes(id)); + if (allSelected) { + this.roleForm.permissions = this.roleForm.permissions.filter(id => !permIds.includes(id)); + } else { + for (const id of permIds) { + if (!this.roleForm.permissions.includes(id)) { + this.roleForm.permissions.push(id); + } + } + } + }, + + isCategoryFullySelected(category) { + const perms = category.permissions || []; + return perms.length > 0 && perms.every(p => this.roleForm.permissions.includes(p.id)); + }, + + async saveRole() { + if (!this.selectedStore) return; + + this.saving = true; + try { + const storeParam = `store_id=${this.selectedStore.id}`; + if (this.editingRole) { + await apiClient.put(`/admin/store-roles/${this.editingRole.id}?${storeParam}`, this.roleForm); + } else { + await apiClient.post(`/admin/store-roles?${storeParam}`, this.roleForm); + } + + this.showRoleModal = false; + Utils.showToast('Role saved successfully', 'success'); + await this.loadRoles(); + } catch (error) { + storeRolesAdminLog.error('Error saving role:', error); + Utils.showToast(error.message || 'Failed to save role', 'error'); + } finally { + this.saving = false; + } + }, + + async confirmDelete(role) { + if (!this.selectedStore) return; + if (!confirm(`Delete role "${role.name}"? This cannot be undone.`)) return; + + try { + await apiClient.delete(`/admin/store-roles/${role.id}?store_id=${this.selectedStore.id}`); + Utils.showToast('Role deleted successfully', 'success'); + await this.loadRoles(); + } catch (error) { + storeRolesAdminLog.error('Error deleting role:', error); + Utils.showToast(error.message || 'Failed to delete role', 'error'); + } + }, + }; +} + +storeRolesAdminLog.info('Module loaded'); diff --git a/app/modules/tenancy/static/store/js/roles.js b/app/modules/tenancy/static/store/js/roles.js index 17cbe395..c59873ec 100644 --- a/app/modules/tenancy/static/store/js/roles.js +++ b/app/modules/tenancy/static/store/js/roles.js @@ -27,7 +27,7 @@ function storeRoles() { showRoleModal: false, editingRole: null, roleForm: { name: '', permissions: [] }, - permissionsByCategory: {}, + permissionCategories: [], presetRoles: ['manager', 'staff', 'support', 'viewer', 'marketing'], async init() { @@ -50,40 +50,14 @@ function storeRoles() { async loadPermissions() { try { - // Group known permissions by category prefix - const allPerms = window.USER_PERMISSIONS || []; - this.permissionsByCategory = this.groupPermissions(allPerms); + const response = await apiClient.get('/store/team/permissions/catalog'); + this.permissionCategories = response.categories || []; + storeRolesLog.info('Loaded permission catalog:', this.permissionCategories.length, 'categories'); } catch (e) { - storeRolesLog.warn('Could not load permission categories:', e); + storeRolesLog.warn('Could not load permission catalog:', e); } }, - groupPermissions(permIds) { - // Known permission categories from the codebase - const knownPerms = [ - 'dashboard.view', - 'settings.view', 'settings.edit', 'settings.theme', 'settings.domains', - 'products.view', 'products.create', 'products.edit', 'products.delete', 'products.import', 'products.export', - 'orders.view', 'orders.edit', 'orders.cancel', 'orders.refund', - 'customers.view', 'customers.edit', 'customers.delete', 'customers.export', - 'stock.view', 'stock.edit', 'stock.transfer', - 'team.view', 'team.invite', 'team.edit', 'team.remove', - 'analytics.view', 'analytics.export', - 'messaging.view_messages', 'messaging.send_messages', 'messaging.manage_templates', - 'billing.view_tiers', 'billing.manage_tiers', 'billing.view_subscriptions', 'billing.manage_subscriptions', 'billing.view_invoices', - 'cms.view_pages', 'cms.manage_pages', 'cms.view_media', 'cms.manage_media', 'cms.manage_themes', - 'loyalty.view_programs', 'loyalty.manage_programs', 'loyalty.view_rewards', 'loyalty.manage_rewards', - 'cart.view', 'cart.manage', - ]; - const groups = {}; - for (const perm of knownPerms) { - const cat = perm.split('.')[0]; - if (!groups[cat]) groups[cat] = []; - groups[cat].push({ id: perm }); - } - return groups; - }, - async loadRoles() { this.loading = true; this.error = false; @@ -127,7 +101,7 @@ function storeRoles() { }, toggleCategory(category) { - const perms = this.permissionsByCategory[category] || []; + const perms = category.permissions || []; const permIds = perms.map(p => p.id); const allSelected = permIds.every(id => this.roleForm.permissions.includes(id)); if (allSelected) { @@ -142,7 +116,7 @@ function storeRoles() { }, isCategoryFullySelected(category) { - const perms = this.permissionsByCategory[category] || []; + const perms = category.permissions || []; return perms.length > 0 && perms.every(p => this.roleForm.permissions.includes(p.id)); }, diff --git a/app/modules/tenancy/templates/tenancy/admin/store-roles.html b/app/modules/tenancy/templates/tenancy/admin/store-roles.html new file mode 100644 index 00000000..e4a080ab --- /dev/null +++ b/app/modules/tenancy/templates/tenancy/admin/store-roles.html @@ -0,0 +1,271 @@ +{# app/templates/admin/store-roles.html #} +{% extends "admin/base.html" %} +{% from 'shared/macros/headers.html' import page_header %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} +{% from 'shared/macros/modals.html' import modal_simple %} + +{% block title %}Store Roles{% endblock %} + +{% block extra_head %} + + +{% endblock %} + +{% block alpine_data %}adminStoreRoles(){% endblock %} + +{% block content %} +{{ page_header('Store Roles', subtitle='Manage roles and permissions for any store') }} + + +
+ {% if is_super_admin %} + +
+
+ + +
+
+ +
+ Select a merchant first +
+
+ +
+
+
+ {% else %} + +
+ + +
+ {% endif %} +
+ + +
+
+
+
+ +
+

Managing Roles For

+

+ {% if is_super_admin %} +

+ {% endif %} +
+
+
+ + +
+
+
+
+ + +
+
+

Loading roles...

+
+ + +
+ + + +
+ + +
+ + {% if is_super_admin %} +

Select a merchant and store above to manage roles

+ {% else %} +

Select a store above to manage its roles

+ {% endif %} +
+ + +{% call modal_simple('roleModal', 'editingRole ? "Edit Role" : "Create Role"', 'showRoleModal') %} +
+ +
+ + +
+ + +
+ +
+ +
+
+ + +
+ + +
+
+{% endcall %} +{% endblock %} + +{% block extra_scripts %} + + + +{% endblock %} diff --git a/app/modules/tenancy/templates/tenancy/store/roles.html b/app/modules/tenancy/templates/tenancy/store/roles.html index 83a0817c..31611418 100644 --- a/app/modules/tenancy/templates/tenancy/store/roles.html +++ b/app/modules/tenancy/templates/tenancy/store/roles.html @@ -101,10 +101,10 @@
-