- Add admin marketplace products page to browse imported products - Add admin vendor products page to manage vendor catalog - Add product detail pages for both marketplace and vendor products - Implement copy-to-vendor API to copy marketplace products to vendor catalogs - Add vendor product service with CRUD operations - Update sidebar navigation with new product management links - Add integration and unit tests for new endpoints and services 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
182 lines
6.0 KiB
JavaScript
182 lines
6.0 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;
|
|
}
|
|
}
|
|
};
|
|
}
|