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:
@@ -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 = '';
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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();
|
||||
|
||||
35
app/modules/catalog/static/vendor/js/products.js
vendored
35
app/modules/catalog/static/vendor/js/products.js
vendored
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user