feat: add unified admin Marketplace Letzshop page
- Add new Marketplace section in admin sidebar with Letzshop sub-item
- Remove old Import and Letzshop Orders items from Product Catalog
- Create unified Letzshop management page with 3 tabs:
- Products tab: Import/Export functionality
- Orders tab: Order management with confirm/reject/tracking
- Settings tab: API credentials and CSV URLs
- Add unified jobs table showing imports, exports, and order syncs
- Implement vendor autocomplete using Tom Select library (CDN + fallback)
- Add /vendors/{vendor_id}/jobs API endpoint for unified job listing
- Move database queries to service layer (LetzshopOrderService)
- Add LetzshopJobItem and LetzshopJobsListResponse schemas
- Include Tom Select CSS/JS assets as local fallback
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ function data() {
|
||||
const defaultSections = {
|
||||
platformAdmin: true,
|
||||
productCatalog: false,
|
||||
marketplace: false,
|
||||
contentMgmt: false,
|
||||
devTools: false,
|
||||
platformHealth: false,
|
||||
@@ -66,7 +67,8 @@ function data() {
|
||||
// Product Catalog
|
||||
'marketplace-products': 'productCatalog',
|
||||
'vendor-products': 'productCatalog',
|
||||
marketplace: 'productCatalog',
|
||||
// Marketplace
|
||||
'marketplace-letzshop': 'marketplace',
|
||||
// Content Management
|
||||
'platform-homepage': 'contentMgmt',
|
||||
'content-pages': 'contentMgmt',
|
||||
|
||||
763
static/admin/js/marketplace-letzshop.js
Normal file
763
static/admin/js/marketplace-letzshop.js
Normal file
@@ -0,0 +1,763 @@
|
||||
// static/admin/js/marketplace-letzshop.js
|
||||
/**
|
||||
* Admin marketplace Letzshop management page logic
|
||||
* Unified page for Products (Import/Export), Orders, and Settings
|
||||
*/
|
||||
|
||||
// Use centralized logger
|
||||
const marketplaceLetzshopLog = window.LogConfig.createLogger('MARKETPLACE-LETZSHOP');
|
||||
|
||||
marketplaceLetzshopLog.info('Loading...');
|
||||
|
||||
function adminMarketplaceLetzshop() {
|
||||
marketplaceLetzshopLog.info('adminMarketplaceLetzshop() called');
|
||||
|
||||
return {
|
||||
// Inherit base layout state
|
||||
...data(),
|
||||
|
||||
// Set page identifier
|
||||
currentPage: 'marketplace-letzshop',
|
||||
|
||||
// Tab state
|
||||
activeTab: 'products',
|
||||
|
||||
// Loading states
|
||||
loading: false,
|
||||
importing: false,
|
||||
exporting: false,
|
||||
importingOrders: false,
|
||||
loadingOrders: false,
|
||||
loadingJobs: false,
|
||||
savingCredentials: false,
|
||||
savingCsvUrls: false,
|
||||
testingConnection: false,
|
||||
submittingTracking: false,
|
||||
|
||||
// Messages
|
||||
error: '',
|
||||
successMessage: '',
|
||||
|
||||
// Tom Select instance
|
||||
tomSelectInstance: null,
|
||||
|
||||
// Selected vendor
|
||||
selectedVendor: null,
|
||||
|
||||
// Letzshop status for selected vendor
|
||||
letzshopStatus: {
|
||||
is_configured: false,
|
||||
auto_sync_enabled: false,
|
||||
last_sync_at: null,
|
||||
last_sync_status: null
|
||||
},
|
||||
|
||||
// Credentials
|
||||
credentials: null,
|
||||
showApiKey: false,
|
||||
|
||||
// Import form
|
||||
importForm: {
|
||||
csv_url: '',
|
||||
language: 'fr',
|
||||
batch_size: 1000
|
||||
},
|
||||
|
||||
// Export settings
|
||||
exportLanguage: 'fr',
|
||||
exportIncludeInactive: false,
|
||||
|
||||
// Settings form
|
||||
settingsForm: {
|
||||
api_key: '',
|
||||
auto_sync_enabled: false,
|
||||
sync_interval_minutes: 15,
|
||||
letzshop_csv_url_fr: '',
|
||||
letzshop_csv_url_en: '',
|
||||
letzshop_csv_url_de: ''
|
||||
},
|
||||
|
||||
// Orders
|
||||
orders: [],
|
||||
totalOrders: 0,
|
||||
ordersPage: 1,
|
||||
ordersLimit: 20,
|
||||
ordersFilter: '',
|
||||
orderStats: { pending: 0, confirmed: 0, rejected: 0, shipped: 0 },
|
||||
|
||||
// Jobs
|
||||
jobs: [],
|
||||
jobsFilter: { type: '', status: '' },
|
||||
jobsPagination: { page: 1, per_page: 10, total: 0 },
|
||||
|
||||
// Modals
|
||||
showTrackingModal: false,
|
||||
showOrderModal: false,
|
||||
selectedOrder: null,
|
||||
trackingForm: { tracking_number: '', tracking_carrier: '' },
|
||||
|
||||
async init() {
|
||||
marketplaceLetzshopLog.info('init() called');
|
||||
|
||||
// Guard against multiple initialization
|
||||
if (window._marketplaceLetzshopInitialized) {
|
||||
marketplaceLetzshopLog.warn('Already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
window._marketplaceLetzshopInitialized = true;
|
||||
|
||||
// Initialize Tom Select after a short delay to ensure DOM is ready
|
||||
this.$nextTick(() => {
|
||||
this.initTomSelect();
|
||||
});
|
||||
|
||||
marketplaceLetzshopLog.info('Initialization complete');
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize Tom Select for vendor autocomplete
|
||||
*/
|
||||
initTomSelect() {
|
||||
const selectEl = this.$refs.vendorSelect;
|
||||
if (!selectEl) {
|
||||
marketplaceLetzshopLog.error('Vendor select element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for TomSelect to be available
|
||||
if (typeof TomSelect === 'undefined') {
|
||||
marketplaceLetzshopLog.warn('TomSelect not loaded yet, retrying...');
|
||||
setTimeout(() => this.initTomSelect(), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
marketplaceLetzshopLog.info('Initializing Tom Select');
|
||||
|
||||
this.tomSelectInstance = new TomSelect(selectEl, {
|
||||
valueField: 'id',
|
||||
labelField: 'name',
|
||||
searchField: ['name', 'vendor_code'],
|
||||
maxOptions: 50,
|
||||
placeholder: 'Search vendor by name or code...',
|
||||
load: async (query, callback) => {
|
||||
if (query.length < 2) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/vendors?search=${encodeURIComponent(query)}&limit=50`);
|
||||
const vendors = response.vendors.map(v => ({
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
vendor_code: v.vendor_code
|
||||
}));
|
||||
callback(vendors);
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to search vendors:', error);
|
||||
callback([]);
|
||||
}
|
||||
},
|
||||
render: {
|
||||
option: (data, escape) => {
|
||||
return `<div class="flex justify-between items-center">
|
||||
<span>${escape(data.name)}</span>
|
||||
<span class="text-xs text-gray-400 ml-2">${escape(data.vendor_code)}</span>
|
||||
</div>`;
|
||||
},
|
||||
item: (data, escape) => {
|
||||
return `<div>${escape(data.name)} <span class="text-gray-400">(${escape(data.vendor_code)})</span></div>`;
|
||||
}
|
||||
},
|
||||
onChange: async (value) => {
|
||||
if (value) {
|
||||
await this.selectVendor(parseInt(value));
|
||||
} else {
|
||||
this.clearVendorSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle vendor selection
|
||||
*/
|
||||
async selectVendor(vendorId) {
|
||||
marketplaceLetzshopLog.info('Selecting vendor:', vendorId);
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
// Load vendor details
|
||||
const vendor = await apiClient.get(`/admin/vendors/${vendorId}`);
|
||||
this.selectedVendor = vendor;
|
||||
|
||||
// Pre-fill settings form with CSV URLs
|
||||
this.settingsForm.letzshop_csv_url_fr = vendor.letzshop_csv_url_fr || '';
|
||||
this.settingsForm.letzshop_csv_url_en = vendor.letzshop_csv_url_en || '';
|
||||
this.settingsForm.letzshop_csv_url_de = vendor.letzshop_csv_url_de || '';
|
||||
|
||||
// Load Letzshop status and credentials
|
||||
await this.loadLetzshopStatus();
|
||||
|
||||
// Load orders and jobs
|
||||
await Promise.all([
|
||||
this.loadOrders(),
|
||||
this.loadJobs()
|
||||
]);
|
||||
|
||||
marketplaceLetzshopLog.info('Vendor loaded:', vendor.name);
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to load vendor:', error);
|
||||
this.error = error.message || 'Failed to load vendor';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear vendor selection
|
||||
*/
|
||||
clearVendorSelection() {
|
||||
this.selectedVendor = null;
|
||||
this.letzshopStatus = { is_configured: false };
|
||||
this.credentials = null;
|
||||
this.orders = [];
|
||||
this.jobs = [];
|
||||
this.settingsForm = {
|
||||
api_key: '',
|
||||
auto_sync_enabled: false,
|
||||
sync_interval_minutes: 15,
|
||||
letzshop_csv_url_fr: '',
|
||||
letzshop_csv_url_en: '',
|
||||
letzshop_csv_url_de: ''
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Load Letzshop status and credentials for selected vendor
|
||||
*/
|
||||
async loadLetzshopStatus() {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/letzshop/vendors/${this.selectedVendor.id}/credentials`);
|
||||
this.credentials = response;
|
||||
this.letzshopStatus = {
|
||||
is_configured: true,
|
||||
auto_sync_enabled: response.auto_sync_enabled,
|
||||
last_sync_at: response.last_sync_at,
|
||||
last_sync_status: response.last_sync_status
|
||||
};
|
||||
this.settingsForm.auto_sync_enabled = response.auto_sync_enabled;
|
||||
this.settingsForm.sync_interval_minutes = response.sync_interval_minutes || 15;
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
// Not configured
|
||||
this.letzshopStatus = { is_configured: false };
|
||||
this.credentials = null;
|
||||
} else {
|
||||
marketplaceLetzshopLog.error('Failed to load Letzshop status:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh all data for selected vendor
|
||||
*/
|
||||
async refreshData() {
|
||||
if (!this.selectedVendor) return;
|
||||
await this.selectVendor(this.selectedVendor.id);
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// PRODUCTS TAB - IMPORT
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Quick fill import form from vendor CSV URLs
|
||||
*/
|
||||
quickFillImport(language) {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
const urlMap = {
|
||||
'fr': this.selectedVendor.letzshop_csv_url_fr,
|
||||
'en': this.selectedVendor.letzshop_csv_url_en,
|
||||
'de': this.selectedVendor.letzshop_csv_url_de
|
||||
};
|
||||
|
||||
const url = urlMap[language];
|
||||
if (url) {
|
||||
this.importForm.csv_url = url;
|
||||
this.importForm.language = language;
|
||||
marketplaceLetzshopLog.info('Quick filled import form:', language, url);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Start product import
|
||||
*/
|
||||
async startImport() {
|
||||
if (!this.selectedVendor || !this.importForm.csv_url) return;
|
||||
|
||||
this.importing = true;
|
||||
this.error = '';
|
||||
this.successMessage = '';
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
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 = '';
|
||||
await this.loadJobs();
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to start import:', error);
|
||||
this.error = error.message || 'Failed to start import';
|
||||
} finally {
|
||||
this.importing = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// PRODUCTS TAB - EXPORT
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Download product export CSV
|
||||
*/
|
||||
async downloadExport() {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
this.exporting = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
language: this.exportLanguage,
|
||||
include_inactive: this.exportIncludeInactive.toString()
|
||||
});
|
||||
|
||||
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';
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to export:', error);
|
||||
this.error = error.message || 'Failed to export products';
|
||||
} finally {
|
||||
this.exporting = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ORDERS TAB
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Load orders for selected vendor
|
||||
*/
|
||||
async loadOrders() {
|
||||
if (!this.selectedVendor || !this.letzshopStatus.is_configured) {
|
||||
this.orders = [];
|
||||
this.totalOrders = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingOrders = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
skip: ((this.ordersPage - 1) * this.ordersLimit).toString(),
|
||||
limit: this.ordersLimit.toString()
|
||||
});
|
||||
|
||||
if (this.ordersFilter) {
|
||||
params.append('sync_status', this.ordersFilter);
|
||||
}
|
||||
|
||||
const response = await apiClient.get(`/admin/letzshop/vendors/${this.selectedVendor.id}/orders?${params}`);
|
||||
this.orders = response.orders || [];
|
||||
this.totalOrders = response.total || 0;
|
||||
|
||||
// Update order stats
|
||||
this.updateOrderStats();
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to load orders:', error);
|
||||
this.error = error.message || 'Failed to load orders';
|
||||
} finally {
|
||||
this.loadingOrders = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update order stats based on current orders
|
||||
*/
|
||||
updateOrderStats() {
|
||||
// Reset stats
|
||||
this.orderStats = { pending: 0, confirmed: 0, rejected: 0, shipped: 0 };
|
||||
|
||||
// Count from orders list
|
||||
for (const order of this.orders) {
|
||||
if (this.orderStats.hasOwnProperty(order.sync_status)) {
|
||||
this.orderStats[order.sync_status]++;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Import orders from Letzshop
|
||||
*/
|
||||
async importOrders() {
|
||||
if (!this.selectedVendor || !this.letzshopStatus.is_configured) return;
|
||||
|
||||
this.importingOrders = true;
|
||||
this.error = '';
|
||||
this.successMessage = '';
|
||||
|
||||
try {
|
||||
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/sync`);
|
||||
this.successMessage = 'Orders imported successfully';
|
||||
await this.loadOrders();
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to import orders:', error);
|
||||
this.error = error.message || 'Failed to import orders';
|
||||
} finally {
|
||||
this.importingOrders = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm an order
|
||||
*/
|
||||
async confirmOrder(order) {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
try {
|
||||
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/confirm`);
|
||||
this.successMessage = 'Order confirmed';
|
||||
await this.loadOrders();
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to confirm order:', error);
|
||||
this.error = error.message || 'Failed to confirm order';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reject an order
|
||||
*/
|
||||
async rejectOrder(order) {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
if (!confirm('Are you sure you want to reject this order?')) return;
|
||||
|
||||
try {
|
||||
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/reject`);
|
||||
this.successMessage = 'Order rejected';
|
||||
await this.loadOrders();
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to reject order:', error);
|
||||
this.error = error.message || 'Failed to reject order';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open tracking modal
|
||||
*/
|
||||
openTrackingModal(order) {
|
||||
this.selectedOrder = order;
|
||||
this.trackingForm = {
|
||||
tracking_number: order.tracking_number || '',
|
||||
tracking_carrier: order.tracking_carrier || ''
|
||||
};
|
||||
this.showTrackingModal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit tracking information
|
||||
*/
|
||||
async submitTracking() {
|
||||
if (!this.selectedVendor || !this.selectedOrder) return;
|
||||
|
||||
this.submittingTracking = true;
|
||||
|
||||
try {
|
||||
await apiClient.post(
|
||||
`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${this.selectedOrder.id}/tracking`,
|
||||
this.trackingForm
|
||||
);
|
||||
this.successMessage = 'Tracking information saved';
|
||||
this.showTrackingModal = false;
|
||||
await this.loadOrders();
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to save tracking:', error);
|
||||
this.error = error.message || 'Failed to save tracking';
|
||||
} finally {
|
||||
this.submittingTracking = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* View order details
|
||||
*/
|
||||
viewOrderDetails(order) {
|
||||
this.selectedOrder = order;
|
||||
this.showOrderModal = true;
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SETTINGS TAB
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Save Letzshop credentials
|
||||
*/
|
||||
async saveCredentials() {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
this.savingCredentials = true;
|
||||
this.error = '';
|
||||
this.successMessage = '';
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
auto_sync_enabled: this.settingsForm.auto_sync_enabled,
|
||||
sync_interval_minutes: parseInt(this.settingsForm.sync_interval_minutes)
|
||||
};
|
||||
|
||||
// Only include API key if it was provided (not just placeholder)
|
||||
if (this.settingsForm.api_key && this.settingsForm.api_key.length > 0) {
|
||||
payload.api_key = this.settingsForm.api_key;
|
||||
}
|
||||
|
||||
if (this.credentials) {
|
||||
// Update existing
|
||||
await apiClient.patch(`/admin/letzshop/vendors/${this.selectedVendor.id}/credentials`, payload);
|
||||
} else {
|
||||
// Create new (API key required)
|
||||
if (!payload.api_key) {
|
||||
this.error = 'API key is required for initial setup';
|
||||
this.savingCredentials = false;
|
||||
return;
|
||||
}
|
||||
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/credentials`, payload);
|
||||
}
|
||||
|
||||
this.successMessage = 'Credentials saved successfully';
|
||||
this.settingsForm.api_key = ''; // Clear the input
|
||||
await this.loadLetzshopStatus();
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to save credentials:', error);
|
||||
this.error = error.message || 'Failed to save credentials';
|
||||
} finally {
|
||||
this.savingCredentials = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Test Letzshop connection
|
||||
*/
|
||||
async testConnection() {
|
||||
if (!this.selectedVendor || !this.letzshopStatus.is_configured) return;
|
||||
|
||||
this.testingConnection = true;
|
||||
this.error = '';
|
||||
this.successMessage = '';
|
||||
|
||||
try {
|
||||
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/test`);
|
||||
this.successMessage = 'Connection test successful!';
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Connection test failed:', error);
|
||||
this.error = error.message || 'Connection test failed';
|
||||
} finally {
|
||||
this.testingConnection = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete Letzshop credentials
|
||||
*/
|
||||
async deleteCredentials() {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
if (!confirm('Are you sure you want to remove the Letzshop configuration? This will disable all Letzshop features for this vendor.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/admin/letzshop/vendors/${this.selectedVendor.id}/credentials`);
|
||||
this.successMessage = 'Credentials removed';
|
||||
this.credentials = null;
|
||||
this.letzshopStatus = { is_configured: false };
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to delete credentials:', error);
|
||||
this.error = error.message || 'Failed to remove credentials';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save CSV URLs to vendor
|
||||
*/
|
||||
async saveCsvUrls() {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
this.savingCsvUrls = true;
|
||||
this.error = '';
|
||||
this.successMessage = '';
|
||||
|
||||
try {
|
||||
await apiClient.patch(`/admin/vendors/${this.selectedVendor.id}`, {
|
||||
letzshop_csv_url_fr: this.settingsForm.letzshop_csv_url_fr || null,
|
||||
letzshop_csv_url_en: this.settingsForm.letzshop_csv_url_en || null,
|
||||
letzshop_csv_url_de: this.settingsForm.letzshop_csv_url_de || null
|
||||
});
|
||||
|
||||
// Update local vendor object
|
||||
this.selectedVendor.letzshop_csv_url_fr = this.settingsForm.letzshop_csv_url_fr;
|
||||
this.selectedVendor.letzshop_csv_url_en = this.settingsForm.letzshop_csv_url_en;
|
||||
this.selectedVendor.letzshop_csv_url_de = this.settingsForm.letzshop_csv_url_de;
|
||||
|
||||
this.successMessage = 'CSV URLs saved successfully';
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to save CSV URLs:', error);
|
||||
this.error = error.message || 'Failed to save CSV URLs';
|
||||
} finally {
|
||||
this.savingCsvUrls = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// JOBS TABLE
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Load jobs for selected vendor
|
||||
*/
|
||||
async loadJobs() {
|
||||
if (!this.selectedVendor) {
|
||||
this.jobs = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingJobs = true;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
skip: ((this.jobsPagination.page - 1) * this.jobsPagination.per_page).toString(),
|
||||
limit: this.jobsPagination.per_page.toString()
|
||||
});
|
||||
|
||||
if (this.jobsFilter.type) {
|
||||
params.append('job_type', this.jobsFilter.type);
|
||||
}
|
||||
if (this.jobsFilter.status) {
|
||||
params.append('status', this.jobsFilter.status);
|
||||
}
|
||||
|
||||
const response = await apiClient.get(`/admin/letzshop/vendors/${this.selectedVendor.id}/jobs?${params}`);
|
||||
this.jobs = response.jobs || [];
|
||||
this.jobsPagination.total = response.total || 0;
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to load jobs:', error);
|
||||
// Don't show error for jobs - not critical
|
||||
} finally {
|
||||
this.loadingJobs = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* View job details
|
||||
*/
|
||||
viewJobDetails(job) {
|
||||
// For now, just log - could open a modal
|
||||
marketplaceLetzshopLog.info('View job details:', job);
|
||||
alert(`Job #${job.id}\nType: ${job.type}\nStatus: ${job.status}\nRecords: ${job.records_succeeded}/${job.records_processed}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* View job errors
|
||||
*/
|
||||
async viewJobErrors(job) {
|
||||
if (job.type !== 'import') return;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/marketplace-import-jobs/${job.id}/errors`);
|
||||
const errors = response.errors || [];
|
||||
|
||||
if (errors.length === 0) {
|
||||
alert('No error details available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show errors in alert for now
|
||||
const errorText = errors.slice(0, 10).map(e =>
|
||||
`Row ${e.row_number}: ${e.error_message}`
|
||||
).join('\n');
|
||||
|
||||
alert(`Import Errors (showing first 10):\n\n${errorText}`);
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to load job errors:', error);
|
||||
this.error = 'Failed to load error details';
|
||||
}
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// UTILITIES
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('en-GB', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format duration between two dates
|
||||
*/
|
||||
formatDuration(startDate, endDate) {
|
||||
if (!startDate) return '-';
|
||||
if (!endDate) return 'In progress...';
|
||||
|
||||
try {
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
const diffMs = end - start;
|
||||
|
||||
if (diffMs < 1000) return '<1s';
|
||||
if (diffMs < 60000) return `${Math.round(diffMs / 1000)}s`;
|
||||
if (diffMs < 3600000) return `${Math.round(diffMs / 60000)}m`;
|
||||
return `${Math.round(diffMs / 3600000)}h`;
|
||||
} catch {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user