- Add database fields for language preferences: - Vendor: dashboard_language, storefront_language, storefront_languages - User: preferred_language - Customer: preferred_language - Add language middleware for request-level language detection: - Cookie-based persistence - Browser Accept-Language fallback - Vendor storefront language constraints - Add language API endpoints (/api/v1/language/*): - POST /set - Set language preference - GET /current - Get current language info - GET /list - List available languages - DELETE /clear - Clear preference - Add i18n utilities (app/utils/i18n.py): - JSON-based translation loading - Jinja2 template integration - Language resolution helpers - Add reusable language selector macros for templates - Add languageSelector() Alpine.js component - Add translation files (en, fr, de, lb) in static/locales/ - Add architecture rules documentation for language implementation - Update marketplace-product-detail.js to use native language names 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
229 lines
7.5 KiB
JavaScript
229 lines
7.5 KiB
JavaScript
// static/admin/js/marketplace-product-detail.js
|
|
/**
|
|
* Admin marketplace product detail page logic
|
|
* View and manage individual marketplace products
|
|
*/
|
|
|
|
const adminMarketplaceProductDetailLog = window.LogConfig.loggers.adminMarketplaceProductDetail ||
|
|
window.LogConfig.createLogger('adminMarketplaceProductDetail', false);
|
|
|
|
adminMarketplaceProductDetailLog.info('Loading...');
|
|
|
|
function adminMarketplaceProductDetail() {
|
|
adminMarketplaceProductDetailLog.info('adminMarketplaceProductDetail() called');
|
|
|
|
// Extract product ID from URL
|
|
const pathParts = window.location.pathname.split('/');
|
|
const productId = parseInt(pathParts[pathParts.length - 1]);
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'marketplace-products',
|
|
|
|
// Product ID from URL
|
|
productId: productId,
|
|
|
|
// Loading states
|
|
loading: true,
|
|
error: '',
|
|
|
|
// Product data
|
|
product: null,
|
|
|
|
// Copy to vendor modal state
|
|
showCopyModal: false,
|
|
copying: false,
|
|
copyForm: {
|
|
vendor_id: '',
|
|
skip_existing: true
|
|
},
|
|
targetVendors: [],
|
|
|
|
async init() {
|
|
adminMarketplaceProductDetailLog.info('Marketplace Product Detail init() called, ID:', this.productId);
|
|
|
|
// Guard against multiple initialization
|
|
if (window._adminMarketplaceProductDetailInitialized) {
|
|
adminMarketplaceProductDetailLog.warn('Already initialized, skipping');
|
|
return;
|
|
}
|
|
window._adminMarketplaceProductDetailInitialized = true;
|
|
|
|
// Load data in parallel
|
|
await Promise.all([
|
|
this.loadProduct(),
|
|
this.loadTargetVendors()
|
|
]);
|
|
|
|
adminMarketplaceProductDetailLog.info('Marketplace Product Detail initialization complete');
|
|
},
|
|
|
|
/**
|
|
* Load product details
|
|
*/
|
|
async loadProduct() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const response = await apiClient.get(`/admin/products/${this.productId}`);
|
|
this.product = response;
|
|
adminMarketplaceProductDetailLog.info('Loaded product:', this.product.marketplace_product_id);
|
|
} catch (error) {
|
|
adminMarketplaceProductDetailLog.error('Failed to load product:', error);
|
|
this.error = error.message || 'Failed to load product details';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load target vendors for copy functionality
|
|
*/
|
|
async loadTargetVendors() {
|
|
try {
|
|
const response = await apiClient.get('/admin/vendors?is_active=true&limit=500');
|
|
this.targetVendors = response.vendors || [];
|
|
adminMarketplaceProductDetailLog.info('Loaded target vendors:', this.targetVendors.length);
|
|
} catch (error) {
|
|
adminMarketplaceProductDetailLog.error('Failed to load target vendors:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Open copy modal
|
|
*/
|
|
openCopyModal() {
|
|
this.copyForm.vendor_id = '';
|
|
this.showCopyModal = true;
|
|
adminMarketplaceProductDetailLog.info('Opening copy modal for product:', this.productId);
|
|
},
|
|
|
|
/**
|
|
* Execute copy to vendor catalog
|
|
*/
|
|
async executeCopyToVendor() {
|
|
if (!this.copyForm.vendor_id) {
|
|
this.error = 'Please select a target vendor';
|
|
return;
|
|
}
|
|
|
|
this.copying = true;
|
|
try {
|
|
const response = await apiClient.post('/admin/products/copy-to-vendor', {
|
|
marketplace_product_ids: [this.productId],
|
|
vendor_id: parseInt(this.copyForm.vendor_id),
|
|
skip_existing: this.copyForm.skip_existing
|
|
});
|
|
|
|
adminMarketplaceProductDetailLog.info('Copy result:', response);
|
|
|
|
// Show success message
|
|
const copied = response.copied || 0;
|
|
const skipped = response.skipped || 0;
|
|
const failed = response.failed || 0;
|
|
|
|
let message;
|
|
if (copied > 0) {
|
|
message = 'Product successfully copied to vendor catalog.';
|
|
} else if (skipped > 0) {
|
|
message = 'Product already exists in the vendor catalog.';
|
|
} else {
|
|
message = 'Failed to copy product.';
|
|
}
|
|
|
|
// Close modal
|
|
this.showCopyModal = false;
|
|
|
|
// Show notification
|
|
Utils.showToast(message, copied > 0 ? 'success' : 'warning');
|
|
} catch (error) {
|
|
adminMarketplaceProductDetailLog.error('Failed to copy product:', error);
|
|
this.error = error.message || 'Failed to copy product to vendor catalog';
|
|
} finally {
|
|
this.copying = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Format price for display
|
|
*/
|
|
formatPrice(price, currency = 'EUR') {
|
|
if (price === null || price === undefined) return '-';
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: currency || 'EUR'
|
|
}).format(price);
|
|
},
|
|
|
|
/**
|
|
* Format date for display
|
|
*/
|
|
formatDate(dateString) {
|
|
if (!dateString) return '-';
|
|
try {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
} catch (e) {
|
|
return dateString;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get full language name from ISO code (native names for Luxembourg languages)
|
|
*/
|
|
getLanguageName(code) {
|
|
const languages = {
|
|
'en': 'English',
|
|
'de': 'Deutsch',
|
|
'fr': 'Français',
|
|
'lb': 'Lëtzebuergesch',
|
|
'es': 'Español',
|
|
'it': 'Italiano',
|
|
'nl': 'Nederlands',
|
|
'pt': 'Português',
|
|
'pl': 'Polski',
|
|
'cs': 'Čeština',
|
|
'da': 'Dansk',
|
|
'sv': 'Svenska',
|
|
'fi': 'Suomi',
|
|
'no': 'Norsk',
|
|
'hu': 'Hungarian',
|
|
'ro': 'Romanian',
|
|
'bg': 'Bulgarian',
|
|
'el': 'Greek',
|
|
'sk': 'Slovak',
|
|
'sl': 'Slovenian',
|
|
'hr': 'Croatian',
|
|
'lt': 'Lithuanian',
|
|
'lv': 'Latvian',
|
|
'et': 'Estonian'
|
|
};
|
|
return languages[code?.toLowerCase()] || '';
|
|
},
|
|
|
|
/**
|
|
* Copy text to clipboard
|
|
*/
|
|
async copyToClipboard(text) {
|
|
if (!text) return;
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
Utils.showToast('Copied to clipboard', 'success');
|
|
} catch (err) {
|
|
adminMarketplaceProductDetailLog.error('Failed to copy to clipboard:', err);
|
|
Utils.showToast('Failed to copy to clipboard', 'error');
|
|
}
|
|
}
|
|
};
|
|
}
|