refactor: complete module-driven architecture migration

This commit completes the migration to a fully module-driven architecture:

## Models Migration
- Moved all domain models from models/database/ to their respective modules:
  - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc.
  - cms: MediaFile, VendorTheme
  - messaging: Email, VendorEmailSettings, VendorEmailTemplate
  - core: AdminMenuConfig
- models/database/ now only contains Base and TimestampMixin (infrastructure)

## Schemas Migration
- Moved all domain schemas from models/schema/ to their respective modules:
  - tenancy: company, vendor, admin, team, vendor_domain
  - cms: media, image, vendor_theme
  - messaging: email
- models/schema/ now only contains base.py and auth.py (infrastructure)

## Routes Migration
- Moved admin routes from app/api/v1/admin/ to modules:
  - menu_config.py -> core module
  - modules.py -> tenancy module
  - module_config.py -> tenancy module
- app/api/v1/admin/ now only aggregates auto-discovered module routes

## Menu System
- Implemented module-driven menu system with MenuDiscoveryService
- Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT
- Added MenuItemDefinition and MenuSectionDefinition dataclasses
- Each module now defines its own menu items in definition.py
- MenuService integrates with MenuDiscoveryService for template rendering

## Documentation
- Updated docs/architecture/models-structure.md
- Updated docs/architecture/menu-management.md
- Updated architecture validation rules for new exceptions

## Architecture Validation
- Updated MOD-019 rule to allow base.py in models/schema/
- Created core module exceptions.py and schemas/ directory
- All validation errors resolved (only warnings remain)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 21:02:56 +01:00
parent 09d7d282c6
commit d7a0ff8818
307 changed files with 5536 additions and 3826 deletions

View File

@@ -1,7 +1,8 @@
# app/modules/catalog/definition.py
"""Catalog module definition."""
from app.modules.base import ModuleDefinition
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
from app.modules.enums import FrontendType
module = ModuleDefinition(
code="catalog",
@@ -10,4 +11,25 @@ module = ModuleDefinition(
version="1.0.0",
is_self_contained=True,
requires=["inventory"],
# New module-driven menu definitions
menus={
FrontendType.VENDOR: [
MenuSectionDefinition(
id="products",
label_key="catalog.menu.products_inventory",
icon="package",
order=10,
items=[
MenuItemDefinition(
id="products",
label_key="catalog.menu.all_products",
icon="shopping-bag",
route="/vendor/{vendor_code}/products",
order=10,
is_mandatory=True,
),
],
),
],
},
)

View File

@@ -1,49 +1,64 @@
{
"title": "Produktkatalog",
"description": "Produktkatalogverwaltung für Händler",
"products": {
"title": "Produkte",
"subtitle": "Verwalten Sie Ihren Produktkatalog",
"create": "Produkt erstellen",
"edit": "Produkt bearbeiten",
"delete": "Produkt löschen",
"empty": "Keine Produkte gefunden",
"empty_search": "Keine Produkte entsprechen Ihrer Suche"
},
"product": {
"name": "Produktname",
"description": "Beschreibung",
"sku": "Artikelnummer",
"product": "Produkt",
"add_product": "Produkt hinzufügen",
"edit_product": "Produkt bearbeiten",
"delete_product": "Produkt löschen",
"product_name": "Produktname",
"product_code": "Produktcode",
"sku": "SKU",
"price": "Preis",
"stock": "Bestand",
"status": "Status",
"active": "Aktiv",
"inactive": "Inaktiv"
},
"media": {
"title": "Produktmedien",
"upload": "Bild hochladen",
"delete": "Bild löschen",
"primary": "Als Hauptbild festlegen",
"error": "Medien-Upload fehlgeschlagen"
},
"validation": {
"name_required": "Produktname ist erforderlich",
"price_required": "Preis ist erforderlich",
"invalid_sku": "Ungültiges Artikelnummernformat",
"duplicate_sku": "Artikelnummer existiert bereits"
"sale_price": "Verkaufspreis",
"cost": "Kosten",
"stock": "Lagerbestand",
"in_stock": "Auf Lager",
"out_of_stock": "Nicht auf Lager",
"low_stock": "Geringer Bestand",
"availability": "Verfügbarkeit",
"available": "Verfügbar",
"unavailable": "Nicht verfügbar",
"brand": "Marke",
"category": "Kategorie",
"categories": "Kategorien",
"image": "Bild",
"images": "Bilder",
"main_image": "Hauptbild",
"gallery": "Galerie",
"weight": "Gewicht",
"dimensions": "Abmessungen",
"color": "Farbe",
"size": "Größe",
"material": "Material",
"condition": "Zustand",
"new": "Neu",
"used": "Gebraucht",
"refurbished": "Generalüberholt",
"no_products": "Keine Produkte gefunden",
"search_products": "Produkte suchen...",
"filter_by_category": "Nach Kategorie filtern",
"filter_by_status": "Nach Status filtern",
"sort_by": "Sortieren nach",
"sort_newest": "Neueste",
"sort_oldest": "Älteste",
"sort_price_low": "Preis: Niedrig bis Hoch",
"sort_price_high": "Preis: Hoch bis Niedrig",
"sort_name_az": "Name: A-Z",
"sort_name_za": "Name: Z-A"
},
"messages": {
"created": "Produkt erfolgreich erstellt",
"updated": "Produkt erfolgreich aktualisiert",
"deleted": "Produkt erfolgreich gelöscht",
"not_found": "Produkt nicht gefunden",
"cannot_delete": "Produkt kann nicht gelöscht werden",
"error_loading": "Fehler beim Laden der Produkte"
},
"filters": {
"all_products": "Alle Produkte",
"active_only": "Nur aktive",
"search_placeholder": "Produkte suchen..."
"product_deleted_successfully": "Product deleted successfully",
"please_fill_in_all_required_fields": "Please fill in all required fields",
"product_updated_successfully": "Product updated successfully",
"failed_to_load_media_library": "Failed to load media library",
"no_vendor_associated_with_this_product": "No vendor associated with this product",
"please_select_an_image_file": "Please select an image file",
"image_must_be_less_than_10mb": "Image must be less than 10MB",
"image_uploaded_successfully": "Image uploaded successfully",
"product_removed_from_vendor_catalog": "Product removed from vendor catalog.",
"please_select_a_vendor": "Please select a vendor",
"please_enter_a_product_title_english": "Please enter a product title (English)",
"product_created_successfully": "Product created successfully",
"please_select_a_vendor_first": "Please select a vendor first"
}
}

View File

@@ -1,49 +1,64 @@
{
"title": "Catalogue produits",
"description": "Gestion du catalogue produits pour les vendeurs",
"products": {
"title": "Produits",
"subtitle": "Gérez votre catalogue de produits",
"create": "Créer un produit",
"edit": "Modifier le produit",
"delete": "Supprimer le produit",
"empty": "Aucun produit trouvé",
"empty_search": "Aucun produit ne correspond à votre recherche"
},
"product": {
"name": "Nom du produit",
"description": "Description",
"sku": "Référence",
"product": "Produit",
"add_product": "Ajouter un produit",
"edit_product": "Modifier le produit",
"delete_product": "Supprimer le produit",
"product_name": "Nom du produit",
"product_code": "Code produit",
"sku": "SKU",
"price": "Prix",
"sale_price": "Prix de vente",
"cost": "Coût",
"stock": "Stock",
"status": "Statut",
"active": "Actif",
"inactive": "Inactif"
},
"media": {
"title": "Médias du produit",
"upload": "Télécharger une image",
"delete": "Supprimer l'image",
"primary": "Définir comme image principale",
"error": "Échec du téléchargement"
},
"validation": {
"name_required": "Le nom du produit est requis",
"price_required": "Le prix est requis",
"invalid_sku": "Format de référence invalide",
"duplicate_sku": "La référence existe déjà"
"in_stock": "En stock",
"out_of_stock": "Rupture de stock",
"low_stock": "Stock faible",
"availability": "Disponibilité",
"available": "Disponible",
"unavailable": "Indisponible",
"brand": "Marque",
"category": "Catégorie",
"categories": "Catégories",
"image": "Image",
"images": "Images",
"main_image": "Image principale",
"gallery": "Galerie",
"weight": "Poids",
"dimensions": "Dimensions",
"color": "Couleur",
"size": "Taille",
"material": "Matériau",
"condition": "État",
"new": "Neuf",
"used": "Occasion",
"refurbished": "Reconditionné",
"no_products": "Aucun produit trouvé",
"search_products": "Rechercher des produits...",
"filter_by_category": "Filtrer par catégorie",
"filter_by_status": "Filtrer par statut",
"sort_by": "Trier par",
"sort_newest": "Plus récent",
"sort_oldest": "Plus ancien",
"sort_price_low": "Prix : croissant",
"sort_price_high": "Prix : décroissant",
"sort_name_az": "Nom : A-Z",
"sort_name_za": "Nom : Z-A"
},
"messages": {
"created": "Produit créé avec succès",
"updated": "Produit mis à jour avec succès",
"deleted": "Produit supprimé avec succès",
"not_found": "Produit non trouvé",
"cannot_delete": "Impossible de supprimer le produit",
"error_loading": "Erreur lors du chargement des produits"
},
"filters": {
"all_products": "Tous les produits",
"active_only": "Actifs uniquement",
"search_placeholder": "Rechercher des produits..."
"product_deleted_successfully": "Product deleted successfully",
"please_fill_in_all_required_fields": "Please fill in all required fields",
"product_updated_successfully": "Product updated successfully",
"failed_to_load_media_library": "Failed to load media library",
"no_vendor_associated_with_this_product": "No vendor associated with this product",
"please_select_an_image_file": "Please select an image file",
"image_must_be_less_than_10mb": "Image must be less than 10MB",
"image_uploaded_successfully": "Image uploaded successfully",
"product_removed_from_vendor_catalog": "Product removed from vendor catalog.",
"please_select_a_vendor": "Please select a vendor",
"please_enter_a_product_title_english": "Please enter a product title (English)",
"product_created_successfully": "Product created successfully",
"please_select_a_vendor_first": "Please select a vendor first"
}
}

View File

@@ -1,49 +1,64 @@
{
"title": "Produktkatalog",
"description": "Produktkatalogverwaltung fir Händler",
"products": {
"title": "Produkter",
"subtitle": "Verwalte Äre Produktkatalog",
"create": "Produkt erstellen",
"edit": "Produkt beaarbechten",
"delete": "Produkt läschen",
"empty": "Keng Produkter fonnt",
"empty_search": "Keng Produkter entspriechen Ärer Sich"
},
"product": {
"name": "Produktnumm",
"description": "Beschreiwung",
"sku": "Artikelnummer",
"product": "Produkt",
"add_product": "Produkt derbäisetzen",
"edit_product": "Produkt änneren",
"delete_product": "Produkt läschen",
"product_name": "Produktnumm",
"product_code": "Produktcode",
"sku": "SKU",
"price": "Präis",
"stock": "Bestand",
"status": "Status",
"active": "Aktiv",
"inactive": "Inaktiv"
},
"media": {
"title": "Produktmedien",
"upload": "Bild eroplueden",
"delete": "Bild läschen",
"primary": "Als Haaptbild setzen",
"error": "Medien-Upload feelgeschloen"
},
"validation": {
"name_required": "Produktnumm ass erfuerderlech",
"price_required": "Präis ass erfuerderlech",
"invalid_sku": "Ongëlteg Artikelnummerformat",
"duplicate_sku": "Artikelnummer existéiert schonn"
"sale_price": "Verkafspräis",
"cost": "Käschten",
"stock": "Lager",
"in_stock": "Op Lager",
"out_of_stock": "Net op Lager",
"low_stock": "Niddregen Stock",
"availability": "Disponibilitéit",
"available": "Disponibel",
"unavailable": "Net disponibel",
"brand": "Mark",
"category": "Kategorie",
"categories": "Kategorien",
"image": "Bild",
"images": "Biller",
"main_image": "Haaptbild",
"gallery": "Galerie",
"weight": "Gewiicht",
"dimensions": "Dimensiounen",
"color": "Faarf",
"size": "Gréisst",
"material": "Material",
"condition": "Zoustand",
"new": "Nei",
"used": "Gebraucht",
"refurbished": "Iwwerholl",
"no_products": "Keng Produkter fonnt",
"search_products": "Produkter sichen...",
"filter_by_category": "No Kategorie filteren",
"filter_by_status": "No Status filteren",
"sort_by": "Sortéieren no",
"sort_newest": "Neisten",
"sort_oldest": "Eelsten",
"sort_price_low": "Präis: Niddreg op Héich",
"sort_price_high": "Präis: Héich op Niddreg",
"sort_name_az": "Numm: A-Z",
"sort_name_za": "Numm: Z-A"
},
"messages": {
"created": "Produkt erfollegräich erstallt",
"updated": "Produkt erfollegräich aktualiséiert",
"deleted": "Produkt erfollegräich geläscht",
"not_found": "Produkt net fonnt",
"cannot_delete": "Produkt kann net geläscht ginn",
"error_loading": "Feeler beim Lueden vun de Produkter"
},
"filters": {
"all_products": "All Produkter",
"active_only": "Nëmmen aktiv",
"search_placeholder": "Produkter sichen..."
"product_deleted_successfully": "Product deleted successfully",
"please_fill_in_all_required_fields": "Please fill in all required fields",
"product_updated_successfully": "Product updated successfully",
"failed_to_load_media_library": "Failed to load media library",
"no_vendor_associated_with_this_product": "No vendor associated with this product",
"please_select_an_image_file": "Please select an image file",
"image_must_be_less_than_10mb": "Image must be less than 10MB",
"image_uploaded_successfully": "Image uploaded successfully",
"product_removed_from_vendor_catalog": "Product removed from vendor catalog.",
"please_select_a_vendor": "Please select a vendor",
"please_enter_a_product_title_english": "Please enter a product title (English)",
"product_created_successfully": "Product created successfully",
"please_select_a_vendor_first": "Please select a vendor first"
}
}

View File

@@ -22,7 +22,7 @@ from app.modules.catalog.schemas import (
ProductResponse,
)
from middleware.vendor_context import require_vendor_context
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
router = APIRouter()
logger = logging.getLogger(__name__)

View File

@@ -15,8 +15,8 @@ from sqlalchemy.orm import Session
from app.api.deps import get_db, require_menu_access
from app.modules.core.utils.page_context import get_admin_context
from app.templates_config import templates
from models.database.admin_menu_config import FrontendType
from models.database.user import User
from app.modules.enums import FrontendType
from app.modules.tenancy.models import User
router = APIRouter()

View File

@@ -14,7 +14,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_from_cookie_or_header, get_db
from app.modules.core.utils.page_context import get_vendor_context
from app.templates_config import templates
from models.database.user import User
from app.modules.tenancy.models import User
router = APIRouter()

View File

@@ -16,7 +16,7 @@ from sqlalchemy.orm import Session, joinedload
from app.modules.catalog.exceptions import ProductNotFoundException
from app.modules.catalog.models import Product
from models.database.vendor import Vendor
from app.modules.tenancy.models import Vendor
logger = logging.getLogger(__name__)

View File

@@ -66,6 +66,9 @@ function adminVendorProductCreate() {
},
async init() {
// Load i18n translations
await I18n.loadModule('catalog');
adminVendorProductCreateLog.info('Vendor Product Create init() called');
// Guard against multiple initialization
@@ -166,12 +169,12 @@ function adminVendorProductCreate() {
*/
async createProduct() {
if (!this.form.vendor_id) {
Utils.showToast('Please select a vendor', 'error');
Utils.showToast(I18n.t('catalog.messages.please_select_a_vendor'), 'error');
return;
}
if (!this.form.translations.en.title?.trim()) {
Utils.showToast('Please enter a product title (English)', 'error');
Utils.showToast(I18n.t('catalog.messages.please_enter_a_product_title_english'), 'error');
return;
}
@@ -224,7 +227,7 @@ function adminVendorProductCreate() {
adminVendorProductCreateLog.info('Product created:', response.id);
Utils.showToast('Product created successfully', 'success');
Utils.showToast(I18n.t('catalog.messages.product_created_successfully'), 'success');
// Redirect to the new product's detail page
setTimeout(() => {
@@ -232,7 +235,7 @@ function adminVendorProductCreate() {
}, 1000);
} catch (error) {
adminVendorProductCreateLog.error('Failed to create product:', error);
Utils.showToast(error.message || 'Failed to create product', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_create_product'), 'error');
} finally {
this.saving = false;
}
@@ -274,7 +277,7 @@ function adminVendorProductCreate() {
this.mediaPickerState.total = response.total || 0;
} catch (error) {
adminVendorProductCreateLog.error('Failed to load media library:', error);
Utils.showToast('Failed to load media library', 'error');
Utils.showToast(I18n.t('catalog.messages.failed_to_load_media_library'), 'error');
} finally {
this.mediaPickerState.loading = false;
}
@@ -326,17 +329,17 @@ function adminVendorProductCreate() {
const vendorId = this.form?.vendor_id;
if (!vendorId) {
Utils.showToast('Please select a vendor first', 'error');
Utils.showToast(I18n.t('catalog.messages.please_select_a_vendor_first'), 'error');
return;
}
if (!file.type.startsWith('image/')) {
Utils.showToast('Please select an image file', 'error');
Utils.showToast(I18n.t('catalog.messages.please_select_an_image_file'), 'error');
return;
}
if (file.size > 10 * 1024 * 1024) {
Utils.showToast('Image must be less than 10MB', 'error');
Utils.showToast(I18n.t('catalog.messages.image_must_be_less_than_10mb'), 'error');
return;
}
@@ -355,11 +358,11 @@ function adminVendorProductCreate() {
this.mediaPickerState.media.unshift(response.media);
this.mediaPickerState.total++;
this.toggleMediaSelection(response.media);
Utils.showToast('Image uploaded successfully', 'success');
Utils.showToast(I18n.t('catalog.messages.image_uploaded_successfully'), 'success');
}
} catch (error) {
adminVendorProductCreateLog.error('Failed to upload image:', error);
Utils.showToast(error.message || 'Failed to upload image', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_upload_image'), 'error');
} finally {
this.mediaPickerState.uploading = false;
event.target.value = '';

View File

@@ -76,6 +76,9 @@ function adminVendorProductEdit() {
},
async init() {
// Load i18n translations
await I18n.loadModule('catalog');
adminVendorProductEditLog.info('Vendor Product Edit init() called, ID:', this.productId);
// Guard against multiple initialization
@@ -209,7 +212,7 @@ function adminVendorProductEdit() {
*/
async saveProduct() {
if (!this.isFormValid()) {
Utils.showToast('Please fill in all required fields', 'error');
Utils.showToast(I18n.t('catalog.messages.please_fill_in_all_required_fields'), 'error');
return;
}
@@ -266,7 +269,7 @@ function adminVendorProductEdit() {
adminVendorProductEditLog.info('Product saved:', this.productId);
Utils.showToast('Product updated successfully', 'success');
Utils.showToast(I18n.t('catalog.messages.product_updated_successfully'), 'success');
// Redirect to detail page
setTimeout(() => {
@@ -274,7 +277,7 @@ function adminVendorProductEdit() {
}, 1000);
} catch (error) {
adminVendorProductEditLog.error('Failed to save product:', error);
Utils.showToast(error.message || 'Failed to save product', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_save_product'), 'error');
} finally {
this.saving = false;
}
@@ -316,7 +319,7 @@ function adminVendorProductEdit() {
this.mediaPickerState.total = response.total || 0;
} catch (error) {
adminVendorProductEditLog.error('Failed to load media library:', error);
Utils.showToast('Failed to load media library', 'error');
Utils.showToast(I18n.t('catalog.messages.failed_to_load_media_library'), 'error');
} finally {
this.mediaPickerState.loading = false;
}
@@ -368,17 +371,17 @@ function adminVendorProductEdit() {
const vendorId = this.product?.vendor_id;
if (!vendorId) {
Utils.showToast('No vendor associated with this product', 'error');
Utils.showToast(I18n.t('catalog.messages.no_vendor_associated_with_this_product'), 'error');
return;
}
if (!file.type.startsWith('image/')) {
Utils.showToast('Please select an image file', 'error');
Utils.showToast(I18n.t('catalog.messages.please_select_an_image_file'), 'error');
return;
}
if (file.size > 10 * 1024 * 1024) {
Utils.showToast('Image must be less than 10MB', 'error');
Utils.showToast(I18n.t('catalog.messages.image_must_be_less_than_10mb'), 'error');
return;
}
@@ -397,11 +400,11 @@ function adminVendorProductEdit() {
this.mediaPickerState.media.unshift(response.media);
this.mediaPickerState.total++;
this.toggleMediaSelection(response.media);
Utils.showToast('Image uploaded successfully', 'success');
Utils.showToast(I18n.t('catalog.messages.image_uploaded_successfully'), 'success');
}
} catch (error) {
adminVendorProductEditLog.error('Failed to upload image:', error);
Utils.showToast(error.message || 'Failed to upload image', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_upload_image'), 'error');
} finally {
this.mediaPickerState.uploading = false;
event.target.value = '';

View File

@@ -116,6 +116,9 @@ function adminVendorProducts() {
},
async init() {
// Load i18n translations
await I18n.loadModule('catalog');
adminVendorProductsLog.info('Vendor Products init() called');
// Guard against multiple initialization
@@ -385,7 +388,7 @@ function adminVendorProducts() {
this.productToRemove = null;
// Show success notification
Utils.showToast('Product removed from vendor catalog.', 'success');
Utils.showToast(I18n.t('catalog.messages.product_removed_from_vendor_catalog'), 'success');
// Refresh the list
await this.refresh();

View File

@@ -112,6 +112,9 @@ function vendorProducts() {
},
async init() {
// Load i18n translations
await I18n.loadModule('catalog');
vendorProductsLog.info('Products init() called');
// Guard against multiple initialization
@@ -230,13 +233,13 @@ function vendorProducts() {
await apiClient.put(`/vendor/products/${product.id}/toggle-active`);
product.is_active = !product.is_active;
Utils.showToast(
product.is_active ? 'Product activated' : 'Product deactivated',
product.is_active ? I18n.t('catalog.messages.product_activated') : I18n.t('catalog.messages.product_deactivated'),
'success'
);
vendorProductsLog.info('Toggled product active:', product.id, product.is_active);
} catch (error) {
vendorProductsLog.error('Failed to toggle active:', error);
Utils.showToast(error.message || 'Failed to update product', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_update_product'), 'error');
} finally {
this.saving = false;
}
@@ -251,13 +254,13 @@ function vendorProducts() {
await apiClient.put(`/vendor/products/${product.id}/toggle-featured`);
product.is_featured = !product.is_featured;
Utils.showToast(
product.is_featured ? 'Product marked as featured' : 'Product unmarked as featured',
product.is_featured ? I18n.t('catalog.messages.product_marked_as_featured') : I18n.t('catalog.messages.product_unmarked_as_featured'),
'success'
);
vendorProductsLog.info('Toggled product featured:', product.id, product.is_featured);
} catch (error) {
vendorProductsLog.error('Failed to toggle featured:', error);
Utils.showToast(error.message || 'Failed to update product', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_update_product'), 'error');
} finally {
this.saving = false;
}
@@ -288,7 +291,7 @@ function vendorProducts() {
this.saving = true;
try {
await apiClient.delete(`/vendor/products/${this.selectedProduct.id}`);
Utils.showToast('Product deleted successfully', 'success');
Utils.showToast(I18n.t('catalog.messages.product_deleted_successfully'), 'success');
vendorProductsLog.info('Deleted product:', this.selectedProduct.id);
this.showDeleteModal = false;
@@ -296,7 +299,7 @@ function vendorProducts() {
await this.loadProducts();
} catch (error) {
vendorProductsLog.error('Failed to delete product:', error);
Utils.showToast(error.message || 'Failed to delete product', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_delete_product'), 'error');
} finally {
this.saving = false;
}
@@ -417,12 +420,12 @@ function vendorProducts() {
successCount++;
}
}
Utils.showToast(`${successCount} product(s) activated`, 'success');
Utils.showToast(I18n.t('catalog.messages.products_activated', { count: successCount }), 'success');
this.clearSelection();
await this.loadProducts();
} catch (error) {
vendorProductsLog.error('Bulk activate failed:', error);
Utils.showToast(error.message || 'Failed to activate products', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_activate_products'), 'error');
} finally {
this.saving = false;
}
@@ -445,12 +448,12 @@ function vendorProducts() {
successCount++;
}
}
Utils.showToast(`${successCount} product(s) deactivated`, 'success');
Utils.showToast(I18n.t('catalog.messages.products_deactivated', { count: successCount }), 'success');
this.clearSelection();
await this.loadProducts();
} catch (error) {
vendorProductsLog.error('Bulk deactivate failed:', error);
Utils.showToast(error.message || 'Failed to deactivate products', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_deactivate_products'), 'error');
} finally {
this.saving = false;
}
@@ -473,12 +476,12 @@ function vendorProducts() {
successCount++;
}
}
Utils.showToast(`${successCount} product(s) marked as featured`, 'success');
Utils.showToast(I18n.t('catalog.messages.products_marked_as_featured', { count: successCount }), 'success');
this.clearSelection();
await this.loadProducts();
} catch (error) {
vendorProductsLog.error('Bulk set featured failed:', error);
Utils.showToast(error.message || 'Failed to update products', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_update_product'), 'error');
} finally {
this.saving = false;
}
@@ -501,12 +504,12 @@ function vendorProducts() {
successCount++;
}
}
Utils.showToast(`${successCount} product(s) unmarked as featured`, 'success');
Utils.showToast(I18n.t('catalog.messages.products_unmarked_as_featured', { count: successCount }), 'success');
this.clearSelection();
await this.loadProducts();
} catch (error) {
vendorProductsLog.error('Bulk remove featured failed:', error);
Utils.showToast(error.message || 'Failed to update products', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_update_product'), 'error');
} finally {
this.saving = false;
}
@@ -533,13 +536,13 @@ function vendorProducts() {
await apiClient.delete(`/vendor/products/${productId}`);
successCount++;
}
Utils.showToast(`${successCount} product(s) deleted`, 'success');
Utils.showToast(I18n.t('catalog.messages.products_deleted', { count: successCount }), 'success');
this.showBulkDeleteModal = false;
this.clearSelection();
await this.loadProducts();
} catch (error) {
vendorProductsLog.error('Bulk delete failed:', error);
Utils.showToast(error.message || 'Failed to delete products', 'error');
Utils.showToast(error.message || I18n.t('catalog.messages.failed_to_delete_product'), 'error');
} finally {
this.saving = false;
}