feat(roles): add admin store roles page, permission i18n, and menu integration
Some checks failed
Some checks failed
- Add admin store roles page with merchant→store cascading for superadmin and store-only selection for platform admin - Add permission catalog API with translated labels/descriptions (en/fr/de/lb) - Add permission translations to all 15 module locale files (60 files total) - Add info icon tooltips for permission descriptions in role editor - Add store roles menu item and admin menu item in module definition - Fix store-selector.js URL construction bug when apiEndpoint has query params - Add admin store roles API (CRUD + platform scoping) - Add integration tests for admin store roles and permission catalog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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é"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"])
|
||||
|
||||
181
app/modules/tenancy/routes/api/admin_store_roles.py
Normal file
181
app/modules/tenancy/routes/api/admin_store_roles.py
Normal file
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
419
app/modules/tenancy/static/admin/js/store-roles.js
Normal file
419
app/modules/tenancy/static/admin/js/store-roles.js
Normal file
@@ -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 `<div class="flex justify-between items-center py-1">
|
||||
<span class="font-medium">${escape(data.name)}</span>
|
||||
<span class="text-xs text-gray-400 ml-2">${data.store_count} store(s)</span>
|
||||
</div>`;
|
||||
},
|
||||
item: function(data, escape) {
|
||||
return `<div>${escape(data.name)}</div>`;
|
||||
},
|
||||
no_results: function() {
|
||||
return '<div class="no-results py-2 px-3 text-gray-500">No merchants found</div>';
|
||||
},
|
||||
loading: function() {
|
||||
return '<div class="loading py-2 px-3 text-gray-500">Searching...</div>';
|
||||
}
|
||||
},
|
||||
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');
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
|
||||
271
app/modules/tenancy/templates/tenancy/admin/store-roles.html
Normal file
271
app/modules/tenancy/templates/tenancy/admin/store-roles.html
Normal file
@@ -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 %}
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css" rel="stylesheet">
|
||||
<style>
|
||||
.ts-wrapper { width: 100%; }
|
||||
.ts-control {
|
||||
background-color: rgb(249 250 251) !important;
|
||||
border-color: rgb(209 213 219) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
}
|
||||
.dark .ts-control {
|
||||
background-color: rgb(55 65 81) !important;
|
||||
border-color: rgb(75 85 99) !important;
|
||||
color: rgb(229 231 235) !important;
|
||||
}
|
||||
.ts-dropdown {
|
||||
border-radius: 0.5rem !important;
|
||||
border-color: rgb(209 213 219) !important;
|
||||
}
|
||||
.dark .ts-dropdown {
|
||||
background-color: rgb(55 65 81) !important;
|
||||
border-color: rgb(75 85 99) !important;
|
||||
}
|
||||
.dark .ts-dropdown .option {
|
||||
color: rgb(229 231 235) !important;
|
||||
}
|
||||
.dark .ts-dropdown .option.active {
|
||||
background-color: rgb(75 85 99) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block alpine_data %}adminStoreRoles(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Store Roles', subtitle='Manage roles and permissions for any store') }}
|
||||
|
||||
<!-- Selection Panel -->
|
||||
<div class="px-4 py-6 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
{% if is_super_admin %}
|
||||
<!-- Super Admin: Merchant → Store cascading -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Merchant
|
||||
</label>
|
||||
<select x-ref="merchantSelect" placeholder="Search merchant by name..."></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Store
|
||||
</label>
|
||||
<div x-show="!selectedMerchant" class="px-3 py-2 text-sm text-gray-400 dark:text-gray-500 border rounded-lg dark:border-gray-600 bg-gray-50 dark:bg-gray-700">
|
||||
Select a merchant first
|
||||
</div>
|
||||
<div x-show="selectedMerchant" x-cloak>
|
||||
<select x-ref="storeSelect" placeholder="Select store..."></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Platform Admin: Store only (scoped to their platforms) -->
|
||||
<div class="max-w-md">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Store
|
||||
</label>
|
||||
<select x-ref="storeSelect" placeholder="Search store by name or code..."></select>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Selected Store Info -->
|
||||
<div x-show="selectedStore" x-cloak class="mb-6">
|
||||
<div class="px-4 py-3 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span x-html="$icon('shield-check', 'w-6 h-6 text-purple-600')"></span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-purple-700 dark:text-purple-300">Managing Roles For</p>
|
||||
<p class="text-lg font-semibold text-purple-900 dark:text-purple-100" x-text="selectedStore?.name"></p>
|
||||
{% if is_super_admin %}
|
||||
<p class="text-xs text-purple-600 dark:text-purple-400" x-text="selectedMerchant ? 'Merchant: ' + selectedMerchant.name : ''"></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="openCreateModal()"
|
||||
class="flex items-center px-3 py-1.5 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-1')"></span>
|
||||
Create Role
|
||||
</button>
|
||||
<button
|
||||
@click="clearSelection()"
|
||||
class="flex items-center gap-1 px-3 py-1.5 text-sm text-purple-700 dark:text-purple-300 bg-purple-100 dark:bg-purple-800 rounded-md hover:bg-purple-200 dark:hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<span x-html="$icon('x-mark', 'w-4 h-4')"></span>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div x-show="rolesLoading" class="text-center py-12">
|
||||
<div class="inline-block animate-spin w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full"></div>
|
||||
<p class="mt-4 text-gray-500 dark:text-gray-400">Loading roles...</p>
|
||||
</div>
|
||||
|
||||
<!-- Roles List -->
|
||||
<div x-show="selectedStore && !rolesLoading" class="space-y-6">
|
||||
<template x-for="role in roles" :key="role.id">
|
||||
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="role.name"></h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<span x-text="(role.permissions || []).length"></span> permissions
|
||||
<template x-if="isPresetRole(role.name)">
|
||||
<span class="ml-2 px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-800 rounded-full dark:bg-blue-900 dark:text-blue-200">Preset</span>
|
||||
</template>
|
||||
<template x-if="!isPresetRole(role.name)">
|
||||
<span class="ml-2 px-2 py-0.5 text-xs font-medium bg-green-100 text-green-800 rounded-full dark:bg-green-900 dark:text-green-200">Custom</span>
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="openEditModal(role)"
|
||||
class="px-3 py-1.5 text-sm font-medium text-purple-600 bg-purple-50 rounded-lg hover:bg-purple-100 dark:text-purple-400 dark:bg-purple-900/20 dark:hover:bg-purple-900/40"
|
||||
>
|
||||
<span x-html="$icon('pencil', 'w-4 h-4 inline mr-1')"></span>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
x-show="!isPresetRole(role.name)"
|
||||
@click="confirmDelete(role)"
|
||||
class="px-3 py-1.5 text-sm font-medium text-red-600 bg-red-50 rounded-lg hover:bg-red-100 dark:text-red-400 dark:bg-red-900/20 dark:hover:bg-red-900/40"
|
||||
>
|
||||
<span x-html="$icon('trash', 'w-4 h-4 inline mr-1')"></span>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permission tags -->
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<template x-for="perm in (role.permissions || [])" :key="perm">
|
||||
<span class="px-2 py-0.5 text-xs font-medium bg-gray-100 text-gray-600 rounded dark:bg-gray-700 dark:text-gray-300" x-text="perm"></span>
|
||||
</template>
|
||||
<template x-if="!role.permissions || role.permissions.length === 0">
|
||||
<span class="text-sm text-gray-400 dark:text-gray-500">No permissions assigned</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="roles.length === 0 && !rolesLoading">
|
||||
<div class="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<span x-html="$icon('shield', 'w-12 h-12 mx-auto mb-4 opacity-50')"></span>
|
||||
<p>No roles found for this store.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- No Store Selected -->
|
||||
<div x-show="!selectedStore && !rolesLoading" class="text-center py-12">
|
||||
<span x-html="$icon('shield-check', 'inline w-12 h-12 text-gray-400 mb-4')"></span>
|
||||
{% if is_super_admin %}
|
||||
<p class="text-gray-600 dark:text-gray-400">Select a merchant and store above to manage roles</p>
|
||||
{% else %}
|
||||
<p class="text-gray-600 dark:text-gray-400">Select a store above to manage its roles</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Role Modal -->
|
||||
{% call modal_simple('roleModal', 'editingRole ? "Edit Role" : "Create Role"', 'showRoleModal') %}
|
||||
<div class="space-y-4">
|
||||
<!-- Role Name -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Role Name</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="roleForm.name"
|
||||
placeholder="e.g. Content Editor"
|
||||
class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Permission Matrix -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Permissions</label>
|
||||
<div class="max-h-96 overflow-y-auto border rounded-lg dark:border-gray-600">
|
||||
<template x-for="category in permissionCategories" :key="category.id">
|
||||
<div class="border-b last:border-b-0 dark:border-gray-600">
|
||||
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700/50 flex items-center justify-between">
|
||||
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300 capitalize" x-text="category.label"></span>
|
||||
<button
|
||||
@click="toggleCategory(category)"
|
||||
class="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400"
|
||||
x-text="isCategoryFullySelected(category) ? 'Deselect All' : 'Select All'"
|
||||
></button>
|
||||
</div>
|
||||
<div class="px-4 py-2 grid grid-cols-1 sm:grid-cols-2 gap-1">
|
||||
<template x-for="perm in category.permissions" :key="perm.id">
|
||||
<label class="flex items-start gap-2 py-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="perm.id"
|
||||
:checked="roleForm.permissions.includes(perm.id)"
|
||||
@change="togglePermission(perm.id)"
|
||||
class="w-4 h-4 mt-0.5 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="perm.label"></span>
|
||||
<span
|
||||
x-show="perm.description"
|
||||
:title="perm.description"
|
||||
x-html="$icon('information-circle', 'w-3.5 h-3.5 text-gray-400 dark:text-gray-500 cursor-help')"
|
||||
></span>
|
||||
</span>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500 font-mono" x-text="perm.id"></span>
|
||||
</div>
|
||||
<template x-if="perm.is_owner_only">
|
||||
<span class="ml-auto px-1.5 py-0.5 text-xs bg-amber-100 text-amber-700 rounded dark:bg-amber-900/30 dark:text-amber-400">Owner</span>
|
||||
</template>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<button
|
||||
@click="showRoleModal = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:text-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
>Cancel</button>
|
||||
<button
|
||||
@click="saveRole()"
|
||||
:disabled="saving || !roleForm.name.trim()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
<span x-show="saving" class="inline-block animate-spin mr-1">↻</span>
|
||||
<span x-text="editingRole ? 'Update Role' : 'Create Role'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script>
|
||||
<script>
|
||||
window._adminStoreRolesConfig = { isSuperAdmin: {{ is_super_admin | tojson }} };
|
||||
</script>
|
||||
<script defer src="{{ url_for('tenancy_static', path='admin/js/store-roles.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -101,10 +101,10 @@
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Permissions</label>
|
||||
<div class="max-h-96 overflow-y-auto border rounded-lg dark:border-gray-600">
|
||||
<template x-for="(perms, category) in permissionsByCategory" :key="category">
|
||||
<template x-for="category in permissionCategories" :key="category.id">
|
||||
<div class="border-b last:border-b-0 dark:border-gray-600">
|
||||
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700/50 flex items-center justify-between">
|
||||
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300 capitalize" x-text="category"></span>
|
||||
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300 capitalize" x-text="category.label"></span>
|
||||
<button
|
||||
@click="toggleCategory(category)"
|
||||
class="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400"
|
||||
@@ -112,16 +112,29 @@
|
||||
></button>
|
||||
</div>
|
||||
<div class="px-4 py-2 grid grid-cols-1 sm:grid-cols-2 gap-1">
|
||||
<template x-for="perm in perms" :key="perm.id">
|
||||
<label class="flex items-center gap-2 py-1 cursor-pointer">
|
||||
<template x-for="perm in category.permissions" :key="perm.id">
|
||||
<label class="flex items-start gap-2 py-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="perm.id"
|
||||
:checked="roleForm.permissions.includes(perm.id)"
|
||||
@change="togglePermission(perm.id)"
|
||||
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
|
||||
class="w-4 h-4 mt-0.5 text-purple-600 border-gray-300 rounded focus:ring-purple-500"
|
||||
/>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400" x-text="perm.id"></span>
|
||||
<div class="flex flex-col">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="perm.label"></span>
|
||||
<span
|
||||
x-show="perm.description"
|
||||
:title="perm.description"
|
||||
x-html="$icon('information-circle', 'w-3.5 h-3.5 text-gray-400 dark:text-gray-500 cursor-help')"
|
||||
></span>
|
||||
</span>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500 font-mono" x-text="perm.id"></span>
|
||||
</div>
|
||||
<template x-if="perm.is_owner_only">
|
||||
<span class="ml-auto px-1.5 py-0.5 text-xs bg-amber-100 text-amber-700 rounded dark:bg-amber-900/30 dark:text-amber-400">Owner</span>
|
||||
</template>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
# app/modules/tenancy/tests/integration/test_admin_store_roles_api.py
|
||||
"""
|
||||
Integration tests for admin store role management API endpoints.
|
||||
|
||||
Tests the admin role management endpoints at:
|
||||
/api/v1/admin/store-roles
|
||||
|
||||
Authentication: Uses super_admin_headers and platform_admin_headers
|
||||
fixtures from tests/fixtures/auth_fixtures.py.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.tenancy.models import (
|
||||
Merchant,
|
||||
Platform,
|
||||
Role,
|
||||
Store,
|
||||
StorePlatform,
|
||||
User,
|
||||
)
|
||||
from middleware.auth import AuthManager
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
# ============================================================================
|
||||
|
||||
BASE = "/api/v1/admin/store-roles"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_store_merchant(db):
|
||||
"""Create a merchant for admin role tests."""
|
||||
auth = AuthManager()
|
||||
uid = uuid.uuid4().hex[:8]
|
||||
owner = User(
|
||||
email=f"admin_role_owner_{uid}@test.com",
|
||||
username=f"admin_role_owner_{uid}",
|
||||
hashed_password=auth.hash_password("ownerpass123"),
|
||||
role="merchant_owner",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(owner)
|
||||
db.flush()
|
||||
|
||||
merchant = Merchant(
|
||||
name="Admin Role Test Merchant",
|
||||
owner_user_id=owner.id,
|
||||
contact_email=owner.email,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(merchant)
|
||||
db.commit()
|
||||
db.refresh(merchant)
|
||||
return merchant
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_role_store(db, admin_store_merchant):
|
||||
"""Create a store for admin role tests."""
|
||||
uid = uuid.uuid4().hex[:8]
|
||||
store = Store(
|
||||
merchant_id=admin_store_merchant.id,
|
||||
store_code=f"ADMROLE_{uid.upper()}",
|
||||
subdomain=f"admrole{uid}",
|
||||
name=f"Admin Role Store {uid}",
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(store)
|
||||
db.commit()
|
||||
db.refresh(store)
|
||||
return store
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_role_custom(db, admin_role_store):
|
||||
"""Create a custom role for update/delete tests."""
|
||||
role = Role(
|
||||
store_id=admin_role_store.id,
|
||||
name="admin_test_custom_role",
|
||||
permissions=["products.view", "orders.view"],
|
||||
)
|
||||
db.add(role)
|
||||
db.commit()
|
||||
db.refresh(role)
|
||||
return role
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_platform(db):
|
||||
"""Create a platform for scoping tests."""
|
||||
uid = uuid.uuid4().hex[:8]
|
||||
platform = Platform(
|
||||
code=f"test_{uid}",
|
||||
name=f"Test Platform {uid}",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(platform)
|
||||
db.commit()
|
||||
db.refresh(platform)
|
||||
return platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store_on_platform(db, admin_role_store, test_platform):
|
||||
"""Link the test store to a platform."""
|
||||
sp = StorePlatform(
|
||||
store_id=admin_role_store.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(sp)
|
||||
db.commit()
|
||||
db.refresh(sp)
|
||||
return sp
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Super Admin Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.tenancy
|
||||
class TestAdminListStoreRoles:
|
||||
"""Tests for GET /api/v1/admin/store-roles."""
|
||||
|
||||
def test_list_roles_as_super_admin(
|
||||
self, client, super_admin_headers, admin_role_store
|
||||
):
|
||||
"""Super admin can list roles for any store."""
|
||||
response = client.get(
|
||||
f"{BASE}?store_id={admin_role_store.id}",
|
||||
headers=super_admin_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "roles" in data
|
||||
assert "total" in data
|
||||
# Default preset roles should be created
|
||||
assert data["total"] >= 5
|
||||
|
||||
def test_list_roles_requires_store_id(self, client, super_admin_headers):
|
||||
"""GET /store-roles without store_id returns 422."""
|
||||
response = client.get(BASE, headers=super_admin_headers)
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_list_roles_unauthenticated(self, client, admin_role_store):
|
||||
"""Unauthenticated request is rejected."""
|
||||
response = client.get(f"{BASE}?store_id={admin_role_store.id}")
|
||||
assert response.status_code in (401, 403)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.tenancy
|
||||
class TestAdminCreateStoreRole:
|
||||
"""Tests for POST /api/v1/admin/store-roles."""
|
||||
|
||||
def test_create_role_as_super_admin(
|
||||
self, client, super_admin_headers, admin_role_store
|
||||
):
|
||||
"""Super admin can create a custom role for any store."""
|
||||
response = client.post(
|
||||
f"{BASE}?store_id={admin_role_store.id}",
|
||||
headers=super_admin_headers,
|
||||
json={
|
||||
"name": "admin_created_role",
|
||||
"permissions": ["products.view", "orders.view"],
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["name"] == "admin_created_role"
|
||||
assert "products.view" in data["permissions"]
|
||||
|
||||
def test_create_preset_name_rejected(
|
||||
self, client, super_admin_headers, admin_role_store
|
||||
):
|
||||
"""Cannot create a role with a preset name."""
|
||||
response = client.post(
|
||||
f"{BASE}?store_id={admin_role_store.id}",
|
||||
headers=super_admin_headers,
|
||||
json={
|
||||
"name": "manager",
|
||||
"permissions": ["products.view"],
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.tenancy
|
||||
class TestAdminUpdateStoreRole:
|
||||
"""Tests for PUT /api/v1/admin/store-roles/{role_id}."""
|
||||
|
||||
def test_update_role_as_super_admin(
|
||||
self, client, super_admin_headers, admin_role_store, admin_role_custom
|
||||
):
|
||||
"""Super admin can update a custom role."""
|
||||
response = client.put(
|
||||
f"{BASE}/{admin_role_custom.id}?store_id={admin_role_store.id}",
|
||||
headers=super_admin_headers,
|
||||
json={
|
||||
"name": "renamed_admin_role",
|
||||
"permissions": ["products.view", "products.edit"],
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "renamed_admin_role"
|
||||
assert "products.edit" in data["permissions"]
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.tenancy
|
||||
class TestAdminDeleteStoreRole:
|
||||
"""Tests for DELETE /api/v1/admin/store-roles/{role_id}."""
|
||||
|
||||
def test_delete_role_as_super_admin(
|
||||
self, client, super_admin_headers, admin_role_store, admin_role_custom
|
||||
):
|
||||
"""Super admin can delete a custom role."""
|
||||
response = client.delete(
|
||||
f"{BASE}/{admin_role_custom.id}?store_id={admin_role_store.id}",
|
||||
headers=super_admin_headers,
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
def test_delete_nonexistent_role(
|
||||
self, client, super_admin_headers, admin_role_store
|
||||
):
|
||||
"""Deleting nonexistent role returns 422."""
|
||||
response = client.delete(
|
||||
f"{BASE}/99999?store_id={admin_role_store.id}",
|
||||
headers=super_admin_headers,
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Permission Catalog Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.tenancy
|
||||
class TestAdminPermissionCatalog:
|
||||
"""Tests for GET /api/v1/admin/store-roles/permissions/catalog."""
|
||||
|
||||
def test_catalog_returns_categories(self, client, super_admin_headers):
|
||||
"""GET /permissions/catalog returns categories with permissions."""
|
||||
response = client.get(
|
||||
f"{BASE}/permissions/catalog",
|
||||
headers=super_admin_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "categories" in data
|
||||
assert len(data["categories"]) > 0
|
||||
|
||||
def test_catalog_permission_has_metadata(self, client, super_admin_headers):
|
||||
"""Each permission has id, label, description, and is_owner_only."""
|
||||
response = client.get(
|
||||
f"{BASE}/permissions/catalog",
|
||||
headers=super_admin_headers,
|
||||
)
|
||||
data = response.json()
|
||||
perm = data["categories"][0]["permissions"][0]
|
||||
assert "id" in perm
|
||||
assert "label" in perm
|
||||
assert "description" in perm
|
||||
assert "is_owner_only" in perm
|
||||
@@ -353,3 +353,54 @@ class TestDeleteRole:
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert "preset" in response.json()["message"].lower()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# GET /team/permissions/catalog
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.tenancy
|
||||
class TestPermissionCatalog:
|
||||
"""Tests for GET /api/v1/store/team/permissions/catalog."""
|
||||
|
||||
def test_catalog_returns_categories(self, client, role_auth):
|
||||
"""GET /permissions/catalog returns categories with permissions."""
|
||||
response = client.get(f"{BASE}/permissions/catalog", headers=role_auth)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "categories" in data
|
||||
assert len(data["categories"]) > 0
|
||||
|
||||
def test_catalog_category_has_permissions(self, client, role_auth):
|
||||
"""Each category contains permission items."""
|
||||
response = client.get(f"{BASE}/permissions/catalog", headers=role_auth)
|
||||
data = response.json()
|
||||
for category in data["categories"]:
|
||||
assert "id" in category
|
||||
assert "label" in category
|
||||
assert "permissions" in category
|
||||
assert len(category["permissions"]) > 0
|
||||
|
||||
def test_catalog_permission_has_metadata(self, client, role_auth):
|
||||
"""Each permission has id, label, description, and is_owner_only."""
|
||||
response = client.get(f"{BASE}/permissions/catalog", headers=role_auth)
|
||||
data = response.json()
|
||||
perm = data["categories"][0]["permissions"][0]
|
||||
assert "id" in perm
|
||||
assert "label" in perm
|
||||
assert "description" in perm
|
||||
assert "is_owner_only" in perm
|
||||
|
||||
def test_catalog_includes_team_permissions(self, client, role_auth):
|
||||
"""Catalog includes team permissions from tenancy module."""
|
||||
response = client.get(f"{BASE}/permissions/catalog", headers=role_auth)
|
||||
data = response.json()
|
||||
all_perm_ids = {
|
||||
p["id"]
|
||||
for cat in data["categories"]
|
||||
for p in cat["permissions"]
|
||||
}
|
||||
assert "team.view" in all_perm_ids
|
||||
assert "team.edit" in all_perm_ids
|
||||
|
||||
@@ -12,7 +12,14 @@ from app.modules.tenancy.exceptions import (
|
||||
InvalidRoleException,
|
||||
UserNotFoundException,
|
||||
)
|
||||
from app.modules.tenancy.models import Role, Store, StoreUser, User
|
||||
from app.modules.tenancy.models import (
|
||||
Platform,
|
||||
Role,
|
||||
Store,
|
||||
StorePlatform,
|
||||
StoreUser,
|
||||
User,
|
||||
)
|
||||
from app.modules.tenancy.services.store_team_service import store_team_service
|
||||
|
||||
# =============================================================================
|
||||
@@ -728,3 +735,80 @@ class TestStoreTeamServiceDeleteRole:
|
||||
store_id=team_store.id,
|
||||
role_id=role.id,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ADMIN STORE ACCESS VALIDATION
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.tenancy
|
||||
class TestValidateAdminStoreAccess:
|
||||
"""Tests for validate_admin_store_access()."""
|
||||
|
||||
def test_super_admin_can_access_any_store(self, db, team_store):
|
||||
"""Super admin (accessible_platform_ids=None) can access any store."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
user_ctx = MagicMock()
|
||||
user_ctx.get_accessible_platform_ids.return_value = None
|
||||
|
||||
store = store_team_service.validate_admin_store_access(
|
||||
db, user_ctx, team_store.id
|
||||
)
|
||||
assert store.id == team_store.id
|
||||
|
||||
def test_platform_admin_can_access_store_in_their_platform(self, db, team_store):
|
||||
"""Platform admin can access stores in their assigned platform."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Create a platform and link the store
|
||||
platform = Platform(
|
||||
code=f"test_plat_{uuid.uuid4().hex[:6]}",
|
||||
name="Test Platform",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(platform)
|
||||
db.flush()
|
||||
|
||||
sp = StorePlatform(
|
||||
store_id=team_store.id,
|
||||
platform_id=platform.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(sp)
|
||||
db.flush()
|
||||
|
||||
user_ctx = MagicMock()
|
||||
user_ctx.get_accessible_platform_ids.return_value = [platform.id]
|
||||
|
||||
store = store_team_service.validate_admin_store_access(
|
||||
db, user_ctx, team_store.id
|
||||
)
|
||||
assert store.id == team_store.id
|
||||
|
||||
def test_platform_admin_cannot_access_store_outside_platform(self, db, team_store):
|
||||
"""Platform admin cannot access stores outside their platform."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
user_ctx = MagicMock()
|
||||
# Platform ID 99999 does not have the test store
|
||||
user_ctx.get_accessible_platform_ids.return_value = [99999]
|
||||
|
||||
with pytest.raises(InvalidRoleException, match="do not have access"):
|
||||
store_team_service.validate_admin_store_access(
|
||||
db, user_ctx, team_store.id
|
||||
)
|
||||
|
||||
def test_nonexistent_store_raises_error(self, db):
|
||||
"""Accessing a nonexistent store raises InvalidRoleException."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
user_ctx = MagicMock()
|
||||
user_ctx.get_accessible_platform_ids.return_value = None
|
||||
|
||||
with pytest.raises(InvalidRoleException, match="not found"):
|
||||
store_team_service.validate_admin_store_access(
|
||||
db, user_ctx, 99999
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user