// 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 `
${escape(data.name)} ${escape(data.vendor_code)}
`; }, item: (data, escape) => { return `
${escape(data.name)} (${escape(data.vendor_code)})
`; } }, 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 '-'; } } }; }