Files
orion/static/admin/js/marketplace-letzshop.js
Samir Boulahtit 2e6f8fdb8a feat: update frontend for unified order model
Update all frontend templates and JavaScript to use new unified Order model:
- Orders tab: use status field, processing/cancelled values, items array
- Order detail: use snapshot fields, items array, tracking_provider
- JavaScript: update API params (status vs sync_status), orderStats fields
- Tracking modal: use tracking_provider instead of tracking_carrier
- Order items modal: use items array with item_state field

All status mappings:
- pending → pending (unconfirmed)
- processing → confirmed (at least one item available)
- cancelled → declined (all items unavailable)
- shipped → shipped (with tracking)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 21:38:02 +01:00

1050 lines
40 KiB
JavaScript

// 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,
importingHistorical: false,
loadingOrders: false,
loadingJobs: false,
savingCredentials: false,
savingCsvUrls: false,
testingConnection: false,
submittingTracking: false,
// Historical import state
historicalImportResult: null,
historicalImportJobId: null,
historicalImportProgress: null,
historicalImportPollInterval: null,
// 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: '',
ordersSearch: '',
ordersHasDeclinedItems: false,
orderStats: { pending: 0, processing: 0, shipped: 0, delivered: 0, cancelled: 0, total: 0, has_declined_items: 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_provider: '' },
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.ordersFilter = '';
this.ordersSearch = '';
this.ordersHasDeclinedItems = false;
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('status', this.ordersFilter);
}
if (this.ordersHasDeclinedItems) {
params.append('has_declined_items', 'true');
}
if (this.ordersSearch) {
params.append('search', this.ordersSearch);
}
const response = await apiClient.get(`/admin/letzshop/vendors/${this.selectedVendor.id}/orders?${params}`);
this.orders = response.orders || [];
this.totalOrders = response.total || 0;
// Use server-side stats (counts all orders, not just visible page)
if (response.stats) {
this.orderStats = response.stats;
} else {
// Fallback to client-side calculation for backwards compatibility
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 (fallback method)
*
* Note: Server now returns stats with all orders counted.
* This method is kept as a fallback for backwards compatibility.
*/
updateOrderStats() {
// Reset stats
this.orderStats = { pending: 0, processing: 0, shipped: 0, delivered: 0, cancelled: 0, total: 0, has_declined_items: 0 };
// Count from orders list (only visible page - not accurate for totals)
for (const order of this.orders) {
if (this.orderStats.hasOwnProperty(order.status)) {
this.orderStats[order.status]++;
}
this.orderStats.total++;
}
},
/**
* 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;
}
},
/**
* Import historical orders from Letzshop (confirmed and declined orders)
* Uses background job with polling for progress tracking
*/
async importHistoricalOrders() {
if (!this.selectedVendor || !this.letzshopStatus.is_configured) return;
this.importingHistorical = true;
this.error = '';
this.successMessage = '';
this.historicalImportResult = null;
this.historicalImportProgress = {
status: 'starting',
message: 'Starting historical import...',
current_phase: null,
current_page: 0,
shipments_fetched: 0,
orders_processed: 0,
};
try {
// Start the import job
const response = await apiClient.post(
`/admin/letzshop/vendors/${this.selectedVendor.id}/import-history`
);
this.historicalImportJobId = response.job_id;
marketplaceLetzshopLog.info('Historical import job started:', response);
// Start polling for progress
this.startHistoricalImportPolling();
} catch (error) {
marketplaceLetzshopLog.error('Failed to start historical import:', error);
this.error = error.message || 'Failed to start historical import';
this.importingHistorical = false;
this.historicalImportProgress = null;
}
},
/**
* Start polling for historical import progress
*/
startHistoricalImportPolling() {
// Poll every 2 seconds
this.historicalImportPollInterval = setInterval(async () => {
await this.pollHistoricalImportStatus();
}, 2000);
},
/**
* Poll historical import status
*/
async pollHistoricalImportStatus() {
if (!this.historicalImportJobId || !this.selectedVendor) {
this.stopHistoricalImportPolling();
return;
}
try {
const status = await apiClient.get(
`/admin/letzshop/vendors/${this.selectedVendor.id}/import-history/${this.historicalImportJobId}/status`
);
// Update progress display
this.historicalImportProgress = {
status: status.status,
message: this.formatProgressMessage(status),
current_phase: status.current_phase,
current_page: status.current_page,
total_pages: status.total_pages,
shipments_fetched: status.shipments_fetched,
orders_processed: status.orders_processed,
};
// Check if complete or failed
if (status.status === 'completed' || status.status === 'failed') {
this.stopHistoricalImportPolling();
this.importingHistorical = false;
if (status.status === 'completed') {
// Combine stats from both phases
const confirmed = status.confirmed_stats || {};
const pending = status.declined_stats || {}; // Actually unconfirmed/pending
this.historicalImportResult = {
imported: (confirmed.imported || 0) + (pending.imported || 0),
updated: (confirmed.updated || 0) + (pending.updated || 0),
skipped: (confirmed.skipped || 0) + (pending.skipped || 0),
products_matched: (confirmed.products_matched || 0) + (pending.products_matched || 0),
products_not_found: (confirmed.products_not_found || 0) + (pending.products_not_found || 0),
};
const stats = this.historicalImportResult;
// Build a meaningful summary message
const parts = [];
if (stats.imported > 0) parts.push(`${stats.imported} imported`);
if (stats.updated > 0) parts.push(`${stats.updated} updated`);
if (stats.skipped > 0) parts.push(`${stats.skipped} already synced`);
this.successMessage = parts.length > 0
? `Historical import complete: ${parts.join(', ')}`
: 'Historical import complete: no orders found';
marketplaceLetzshopLog.info('Historical import completed:', status);
// Reload orders to show new data
await this.loadOrders();
} else {
this.error = status.error_message || 'Historical import failed';
marketplaceLetzshopLog.error('Historical import failed:', status);
}
this.historicalImportProgress = null;
this.historicalImportJobId = null;
}
} catch (error) {
marketplaceLetzshopLog.error('Failed to poll import status:', error);
// Don't stop polling on transient errors
}
},
/**
* Stop polling for historical import progress
*/
stopHistoricalImportPolling() {
if (this.historicalImportPollInterval) {
clearInterval(this.historicalImportPollInterval);
this.historicalImportPollInterval = null;
}
},
/**
* Format progress message for display
*/
formatProgressMessage(status) {
// Map phase to display name
const phaseNames = {
'confirmed': 'confirmed',
'unconfirmed': 'pending',
'declined': 'declined', // Legacy support
};
const phase = phaseNames[status.current_phase] || status.current_phase || 'orders';
if (status.status === 'fetching') {
if (status.total_pages) {
return `Fetching ${phase} orders: page ${status.current_page} of ${status.total_pages} (${status.shipments_fetched} fetched)`;
}
return `Fetching ${phase} orders: page ${status.current_page}... (${status.shipments_fetched} fetched)`;
}
if (status.status === 'processing') {
return `Processing ${phase} orders: ${status.orders_processed} processed...`;
}
if (status.status === 'pending') {
return 'Starting historical import...';
}
return status.status.charAt(0).toUpperCase() + status.status.slice(1);
},
/**
* 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';
}
},
/**
* Decline an order (all items)
*/
async declineOrder(order) {
if (!this.selectedVendor) return;
if (!confirm('Are you sure you want to decline this order? All items will be marked as unavailable.')) return;
try {
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/reject`);
this.successMessage = 'Order declined';
await this.loadOrders();
} catch (error) {
marketplaceLetzshopLog.error('Failed to decline order:', error);
this.error = error.message || 'Failed to decline order';
}
},
/**
* Open tracking modal
*/
openTrackingModal(order) {
this.selectedOrder = order;
this.trackingForm = {
tracking_number: order.tracking_number || '',
tracking_provider: order.tracking_provider || ''
};
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;
},
/**
* Confirm a single order item
*/
async confirmInventoryUnit(order, item, index) {
if (!this.selectedVendor) return;
// Use external_item_id (Letzshop inventory unit ID)
const itemId = item.external_item_id;
if (!itemId) {
this.error = 'Item has no external ID';
return;
}
try {
await apiClient.post(
`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/items/${itemId}/confirm`
);
// Update local state
this.selectedOrder.items[index].item_state = 'confirmed_available';
this.successMessage = 'Item confirmed';
// Reload orders to get updated status
await this.loadOrders();
} catch (error) {
marketplaceLetzshopLog.error('Failed to confirm item:', error);
this.error = error.message || 'Failed to confirm item';
}
},
/**
* Decline a single order item
*/
async declineInventoryUnit(order, item, index) {
if (!this.selectedVendor) return;
// Use external_item_id (Letzshop inventory unit ID)
const itemId = item.external_item_id;
if (!itemId) {
this.error = 'Item has no external ID';
return;
}
try {
await apiClient.post(
`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/items/${itemId}/decline`
);
// Update local state
this.selectedOrder.items[index].item_state = 'confirmed_unavailable';
this.successMessage = 'Item declined';
// Reload orders to get updated status
await this.loadOrders();
} catch (error) {
marketplaceLetzshopLog.error('Failed to decline item:', error);
this.error = error.message || 'Failed to decline item';
}
},
/**
* Confirm all items in an order
*/
async confirmAllItems(order) {
if (!this.selectedVendor) return;
if (!confirm('Are you sure you want to confirm all items in this order?')) return;
try {
await apiClient.post(
`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/confirm`
);
this.successMessage = 'All items confirmed';
this.showOrderModal = false;
await this.loadOrders();
} catch (error) {
marketplaceLetzshopLog.error('Failed to confirm all items:', error);
this.error = error.message || 'Failed to confirm all items';
}
},
/**
* Decline all items in an order
*/
async declineAllItems(order) {
if (!this.selectedVendor) return;
if (!confirm('Are you sure you want to decline all items in this order?')) return;
try {
await apiClient.post(
`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/reject`
);
this.successMessage = 'All items declined';
this.showOrderModal = false;
await this.loadOrders();
} catch (error) {
marketplaceLetzshopLog.error('Failed to decline all items:', error);
this.error = error.message || 'Failed to decline all items';
}
},
// ═══════════════════════════════════════════════════════════════
// 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 '-';
}
}
};
}