Files
orion/static/admin/js/marketplace-product-detail.js
Samir Boulahtit c2f42c2913 feat: add import error tracking and translation tabs
Import Error Tracking:
- Add MarketplaceImportError model to store detailed error information
- Store row number, identifier, error type, message, and row data for each error
- Add API endpoint GET /admin/marketplace-import-jobs/{job_id}/errors
- Add UI to view and browse import errors in job details modal
- Support pagination and error type filtering

Translation Tabs:
- Replace flat translation list with tabbed interface on product detail page
- Add language tabs with full language names
- Add copy-to-clipboard functionality for translation content
- Improved UX with better visual separation of translations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 13:33:03 +01:00

229 lines
7.4 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
*/
getLanguageName(code) {
const languages = {
'en': 'English',
'de': 'German',
'fr': 'French',
'lb': 'Luxembourgish',
'es': 'Spanish',
'it': 'Italian',
'nl': 'Dutch',
'pt': 'Portuguese',
'pl': 'Polish',
'cs': 'Czech',
'da': 'Danish',
'sv': 'Swedish',
'fi': 'Finnish',
'no': 'Norwegian',
'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');
}
}
};
}