feat: redesign Letzshop products tab with product listing view
Products Tab Changes:
- Converted to product listing page similar to /admin/marketplace-products
- Added Import/Export buttons in header
- Added product stats cards (total, active, inactive, last sync)
- Added search and filter functionality
- Added product table with pagination
- Import modal for single URL or all languages
Settings Tab Changes:
- Moved batch size setting from products tab
- Moved include inactive checkbox from products tab
- Added export behavior info box
Export Changes:
- New POST endpoint exports all languages (FR, DE, EN)
- CSV files written to exports/letzshop/{vendor_code}/ for scheduler pickup
- Letzshop scheduler can fetch files from this location
API Changes:
- Added vendor_id filter to /admin/vendor-products/stats endpoint
- Added POST /admin/vendors/{id}/export/letzshop for folder export
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,16 @@ function adminMarketplaceLetzshop() {
|
||||
jobsFilter: { type: '', status: '' },
|
||||
jobsPagination: { page: 1, per_page: 10, total: 0 },
|
||||
|
||||
// Products Tab
|
||||
products: [],
|
||||
totalProducts: 0,
|
||||
productsPage: 1,
|
||||
productsLimit: 20,
|
||||
loadingProducts: false,
|
||||
productFilters: { search: '', is_active: '' },
|
||||
productStats: { total: 0, active: 0, inactive: 0, last_sync: null },
|
||||
showImportModal: false,
|
||||
|
||||
// Modals
|
||||
showTrackingModal: false,
|
||||
showOrderModal: false,
|
||||
@@ -300,11 +310,12 @@ function adminMarketplaceLetzshop() {
|
||||
// Load Letzshop status and credentials
|
||||
await this.loadLetzshopStatus();
|
||||
|
||||
// Load orders, exceptions, and jobs
|
||||
// Load orders, exceptions, products, and jobs
|
||||
await Promise.all([
|
||||
this.loadOrders(),
|
||||
this.loadExceptions(),
|
||||
this.loadExceptionStats(),
|
||||
this.loadProducts(),
|
||||
this.loadJobs()
|
||||
]);
|
||||
|
||||
@@ -397,49 +408,143 @@ function adminMarketplaceLetzshop() {
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// PRODUCTS TAB - IMPORT
|
||||
// PRODUCTS TAB - LISTING
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Quick fill import form from vendor CSV URLs
|
||||
* Load products for selected vendor
|
||||
*/
|
||||
quickFillImport(language) {
|
||||
if (!this.selectedVendor) return;
|
||||
async loadProducts() {
|
||||
if (!this.selectedVendor) {
|
||||
this.products = [];
|
||||
this.totalProducts = 0;
|
||||
this.productStats = { total: 0, active: 0, inactive: 0, last_sync: null };
|
||||
return;
|
||||
}
|
||||
|
||||
const urlMap = {
|
||||
'fr': this.selectedVendor.letzshop_csv_url_fr,
|
||||
'en': this.selectedVendor.letzshop_csv_url_en,
|
||||
'de': this.selectedVendor.letzshop_csv_url_de
|
||||
};
|
||||
this.loadingProducts = true;
|
||||
|
||||
const url = urlMap[language];
|
||||
if (url) {
|
||||
this.importForm.csv_url = url;
|
||||
this.importForm.language = language;
|
||||
marketplaceLetzshopLog.info('Quick filled import form:', language, url);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
vendor_id: this.selectedVendor.id.toString(),
|
||||
skip: ((this.productsPage - 1) * this.productsLimit).toString(),
|
||||
limit: this.productsLimit.toString()
|
||||
});
|
||||
|
||||
if (this.productFilters.search) {
|
||||
params.append('search', this.productFilters.search);
|
||||
}
|
||||
|
||||
if (this.productFilters.is_active !== '') {
|
||||
params.append('is_active', this.productFilters.is_active);
|
||||
}
|
||||
|
||||
const response = await apiClient.get(`/admin/vendor-products?${params}`);
|
||||
this.products = response.products || [];
|
||||
this.totalProducts = response.total || 0;
|
||||
|
||||
// Update stats
|
||||
if (response.stats) {
|
||||
this.productStats = response.stats;
|
||||
} else {
|
||||
// Calculate from response if not provided
|
||||
await this.loadProductStats();
|
||||
}
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to load products:', error);
|
||||
this.products = [];
|
||||
this.totalProducts = 0;
|
||||
} finally {
|
||||
this.loadingProducts = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Start product import
|
||||
* Load product statistics
|
||||
*/
|
||||
async startImport() {
|
||||
async loadProductStats() {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/vendor-products/stats?vendor_id=${this.selectedVendor.id}`);
|
||||
this.productStats = {
|
||||
total: response.total || 0,
|
||||
active: response.active || 0,
|
||||
inactive: response.inactive || 0,
|
||||
last_sync: response.last_sync || null
|
||||
};
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to load product stats:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// PRODUCTS TAB - IMPORT
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Import all languages from configured CSV URLs
|
||||
*/
|
||||
async startImportAllLanguages() {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
this.importing = true;
|
||||
this.error = '';
|
||||
this.successMessage = '';
|
||||
this.showImportModal = false;
|
||||
|
||||
try {
|
||||
const languages = [];
|
||||
if (this.selectedVendor.letzshop_csv_url_fr) languages.push({ url: this.selectedVendor.letzshop_csv_url_fr, lang: 'fr' });
|
||||
if (this.selectedVendor.letzshop_csv_url_en) languages.push({ url: this.selectedVendor.letzshop_csv_url_en, lang: 'en' });
|
||||
if (this.selectedVendor.letzshop_csv_url_de) languages.push({ url: this.selectedVendor.letzshop_csv_url_de, lang: 'de' });
|
||||
|
||||
if (languages.length === 0) {
|
||||
this.error = 'No CSV URLs configured. Please set them in Settings.';
|
||||
this.importing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Start import jobs for all languages
|
||||
for (const { url, lang } of languages) {
|
||||
await apiClient.post('/admin/marketplace-import-jobs', {
|
||||
vendor_id: this.selectedVendor.id,
|
||||
source_url: url,
|
||||
marketplace: 'Letzshop',
|
||||
language: lang,
|
||||
batch_size: this.importForm.batch_size
|
||||
});
|
||||
}
|
||||
|
||||
this.successMessage = `Import started for ${languages.length} language(s)`;
|
||||
await this.loadJobs();
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to start import:', error);
|
||||
this.error = error.message || 'Failed to start import';
|
||||
} finally {
|
||||
this.importing = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Import from custom URL
|
||||
*/
|
||||
async startImportFromUrl() {
|
||||
if (!this.selectedVendor || !this.importForm.csv_url) return;
|
||||
|
||||
this.importing = true;
|
||||
this.error = '';
|
||||
this.successMessage = '';
|
||||
this.showImportModal = false;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
await apiClient.post('/admin/marketplace-import-jobs', {
|
||||
vendor_id: this.selectedVendor.id,
|
||||
source_url: this.importForm.csv_url,
|
||||
marketplace: 'Letzshop',
|
||||
language: this.importForm.language,
|
||||
batch_size: this.importForm.batch_size
|
||||
};
|
||||
|
||||
await apiClient.post('/admin/marketplace-import-jobs', payload);
|
||||
});
|
||||
|
||||
this.successMessage = 'Import job started successfully';
|
||||
this.importForm.csv_url = '';
|
||||
@@ -452,36 +557,34 @@ function adminMarketplaceLetzshop() {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Legacy method for backwards compatibility
|
||||
*/
|
||||
async startImport() {
|
||||
return this.startImportFromUrl();
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// PRODUCTS TAB - EXPORT
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Download product export CSV
|
||||
* Export products for all languages to Letzshop pickup folder
|
||||
*/
|
||||
async downloadExport() {
|
||||
async exportAllLanguages() {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
this.exporting = true;
|
||||
this.error = '';
|
||||
this.successMessage = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
language: this.exportLanguage,
|
||||
include_inactive: this.exportIncludeInactive.toString()
|
||||
const response = await apiClient.post(`/admin/vendors/${this.selectedVendor.id}/export/letzshop`, {
|
||||
include_inactive: this.exportIncludeInactive
|
||||
});
|
||||
|
||||
const url = `/api/v1/admin/vendors/${this.selectedVendor.id}/export/letzshop?${params}`;
|
||||
|
||||
// Create a link and trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${this.selectedVendor.vendor_code}_letzshop_export.csv`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.successMessage = 'Export started';
|
||||
this.successMessage = response.message || 'Export completed. CSV files are ready for Letzshop pickup.';
|
||||
marketplaceLetzshopLog.info('Export completed:', response);
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to export:', error);
|
||||
this.error = error.message || 'Failed to export products';
|
||||
@@ -490,6 +593,24 @@ function adminMarketplaceLetzshop() {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Legacy download export method
|
||||
*/
|
||||
async downloadExport() {
|
||||
return this.exportAllLanguages();
|
||||
},
|
||||
|
||||
/**
|
||||
* Format price for display
|
||||
*/
|
||||
formatPrice(price, currency = 'EUR') {
|
||||
if (price == null) return '-';
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(price);
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ORDERS TAB
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user