// static/admin/js/marketplace.js /** * Admin marketplace import page logic */ // ✅ Use centralized logger const adminMarketplaceLog = window.LogConfig.loggers.marketplace; adminMarketplaceLog.info('Loading...'); function adminMarketplace() { adminMarketplaceLog.info('adminMarketplace() called'); return { // ✅ Inherit base layout state ...data(), // ✅ Set page identifier currentPage: 'marketplace', // Loading states loading: false, importing: false, error: '', successMessage: '', // Active import tab (marketplace selector) activeImportTab: 'letzshop', // Vendors list vendors: [], selectedVendor: null, // Import form importForm: { vendor_id: '', csv_url: '', marketplace: 'Letzshop', language: 'fr', batch_size: 1000 }, // Filters filters: { vendor_id: '', status: '', marketplace: '' }, // Import jobs jobs: [], pagination: { page: 1, per_page: 10, total: 0, pages: 0 }, // Modal state showJobModal: false, selectedJob: null, // Auto-refresh for active jobs autoRefreshInterval: null, // Computed: Total pages get totalPages() { return this.pagination.pages; }, // Computed: Start index for pagination display get startIndex() { if (this.pagination.total === 0) return 0; return (this.pagination.page - 1) * this.pagination.per_page + 1; }, // Computed: End index for pagination display get endIndex() { const end = this.pagination.page * this.pagination.per_page; return end > this.pagination.total ? this.pagination.total : end; }, // Computed: Page numbers for pagination get pageNumbers() { const pages = []; const totalPages = this.totalPages; const current = this.pagination.page; if (totalPages <= 7) { for (let i = 1; i <= totalPages; i++) { pages.push(i); } } else { pages.push(1); if (current > 3) { pages.push('...'); } const start = Math.max(2, current - 1); const end = Math.min(totalPages - 1, current + 1); for (let i = start; i <= end; i++) { pages.push(i); } if (current < totalPages - 2) { pages.push('...'); } pages.push(totalPages); } return pages; }, async init() { adminMarketplaceLog.info('Marketplace init() called'); // Guard against multiple initialization if (window._adminMarketplaceInitialized) { adminMarketplaceLog.warn('Already initialized, skipping'); return; } window._adminMarketplaceInitialized = true; // Ensure form defaults are set (in case spread didn't work) if (!this.importForm.marketplace) { this.importForm.marketplace = 'Letzshop'; } if (!this.importForm.batch_size) { this.importForm.batch_size = 1000; } if (!this.importForm.language) { this.importForm.language = 'fr'; } adminMarketplaceLog.info('Form defaults:', this.importForm); await this.loadVendors(); await this.loadJobs(); // Auto-refresh active jobs every 10 seconds this.startAutoRefresh(); adminMarketplaceLog.info('Marketplace initialization complete'); }, /** * Load all vendors for dropdown */ async loadVendors() { try { const response = await apiClient.get('/admin/vendors?limit=1000'); this.vendors = response.vendors || []; adminMarketplaceLog.info('Loaded vendors:', this.vendors.length); } catch (error) { adminMarketplaceLog.error('Failed to load vendors:', error); this.error = 'Failed to load vendors: ' + (error.message || 'Unknown error'); } }, /** * Handle vendor selection change */ onVendorChange() { const vendorId = parseInt(this.importForm.vendor_id); this.selectedVendor = this.vendors.find(v => v.id === vendorId) || null; adminMarketplaceLog.info('Selected vendor:', this.selectedVendor); // Auto-populate CSV URL if marketplace is Letzshop this.autoPopulateCSV(); }, /** * Handle language selection change */ onLanguageChange() { // Auto-populate CSV URL if marketplace is Letzshop this.autoPopulateCSV(); }, /** * Auto-populate CSV URL based on selected vendor and language */ autoPopulateCSV() { // Only auto-populate for Letzshop marketplace if (this.importForm.marketplace !== 'Letzshop') return; 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[this.importForm.language]; if (url) { this.importForm.csv_url = url; adminMarketplaceLog.info('Auto-populated CSV URL:', this.importForm.language, url); } else { adminMarketplaceLog.info('No CSV URL configured for language:', this.importForm.language); } }, /** * Load import jobs (only jobs triggered by current admin user) */ async loadJobs() { this.loading = true; this.error = ''; try { // Build query params const params = new URLSearchParams({ skip: (this.pagination.page - 1) * this.pagination.per_page, limit: this.pagination.per_page, created_by_me: 'true' // ✅ Only show jobs I triggered }); // Add filters (keep for consistency, though less needed here) if (this.filters.vendor_id) { params.append('vendor_id', this.filters.vendor_id); } if (this.filters.status) { params.append('status', this.filters.status); } if (this.filters.marketplace) { params.append('marketplace', this.filters.marketplace); } const response = await apiClient.get( `/admin/marketplace-import-jobs?${params.toString()}` ); this.jobs = response.items || []; this.pagination.total = response.total || 0; this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page); adminMarketplaceLog.info('Loaded my jobs:', this.jobs.length); } catch (error) { adminMarketplaceLog.error('Failed to load jobs:', error); this.error = error.message || 'Failed to load import jobs'; } finally { this.loading = false; } }, /** * Start new import for selected vendor */ async startImport() { if (!this.importForm.csv_url || !this.importForm.vendor_id) { this.error = 'Please select a vendor and enter a CSV URL'; return; } this.importing = true; this.error = ''; this.successMessage = ''; try { const payload = { vendor_id: parseInt(this.importForm.vendor_id), source_url: this.importForm.csv_url, marketplace: this.importForm.marketplace, batch_size: this.importForm.batch_size, language: this.importForm.language // Include selected language }; adminMarketplaceLog.info('Starting import:', payload); const response = await apiClient.post('/admin/marketplace-import-jobs', payload); adminMarketplaceLog.info('Import started:', response); const vendorName = this.selectedVendor?.name || 'vendor'; this.successMessage = `Import job #${response.job_id || response.id} started successfully for ${vendorName}!`; // Clear form this.importForm.vendor_id = ''; this.importForm.csv_url = ''; this.importForm.language = 'fr'; this.importForm.batch_size = 1000; this.selectedVendor = null; // Reload jobs to show the new import await this.loadJobs(); // Clear success message after 5 seconds setTimeout(() => { this.successMessage = ''; }, 5000); } catch (error) { adminMarketplaceLog.error('Failed to start import:', error); this.error = error.message || 'Failed to start import'; } finally { this.importing = false; } }, /** * Switch marketplace tab and update form accordingly */ switchMarketplace(marketplace) { this.activeImportTab = marketplace; // Update marketplace in form const marketplaceMap = { 'letzshop': 'Letzshop', 'codeswholesale': 'CodesWholesale' }; this.importForm.marketplace = marketplaceMap[marketplace] || 'Letzshop'; // Reset form fields when switching tabs this.importForm.vendor_id = ''; this.importForm.csv_url = ''; this.importForm.language = 'fr'; this.importForm.batch_size = 1000; this.selectedVendor = null; adminMarketplaceLog.info('Switched to marketplace:', this.importForm.marketplace); }, /** * Quick fill form with saved CSV URL from vendor settings */ quickFill(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; adminMarketplaceLog.info('Quick filled:', language, url); } }, /** * Clear all filters and reload */ clearFilters() { this.filters.vendor_id = ''; this.filters.status = ''; this.filters.marketplace = ''; this.pagination.page = 1; this.loadJobs(); }, /** * Refresh jobs list */ async refreshJobs() { await this.loadJobs(); }, /** * Refresh single job status */ async refreshJobStatus(jobId) { try { const response = await apiClient.get(`/admin/marketplace-import-jobs/${jobId}`); // Update job in list const index = this.jobs.findIndex(j => j.id === jobId); if (index !== -1) { this.jobs[index] = response; } // Update selected job if modal is open if (this.selectedJob && this.selectedJob.id === jobId) { this.selectedJob = response; } adminMarketplaceLog.info('Refreshed job:', jobId); } catch (error) { adminMarketplaceLog.error('Failed to refresh job:', error); } }, /** * View job details in modal */ async viewJobDetails(jobId) { try { const response = await apiClient.get(`/admin/marketplace-import-jobs/${jobId}`); this.selectedJob = response; this.showJobModal = true; adminMarketplaceLog.info('Viewing job details:', jobId); } catch (error) { adminMarketplaceLog.error('Failed to load job details:', error); this.error = error.message || 'Failed to load job details'; } }, /** * Close job details modal */ closeJobModal() { this.showJobModal = false; this.selectedJob = null; }, /** * Get vendor name by ID */ getVendorName(vendorId) { const vendor = this.vendors.find(v => v.id === vendorId); return vendor ? `${vendor.name} (${vendor.vendor_code})` : `Vendor #${vendorId}`; }, /** * Pagination: Previous page */ previousPage() { if (this.pagination.page > 1) { this.pagination.page--; this.loadJobs(); } }, /** * Pagination: Next page */ nextPage() { if (this.pagination.page < this.totalPages) { this.pagination.page++; this.loadJobs(); } }, /** * Pagination: Go to specific page */ goToPage(pageNum) { if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) { this.pagination.page = pageNum; this.loadJobs(); } }, /** * Format date for display */ formatDate(dateString) { if (!dateString) return 'N/A'; try { const date = new Date(dateString); return date.toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch (error) { return dateString; } }, /** * Calculate duration between start and end */ calculateDuration(job) { if (!job.started_at) { return 'Not started'; } const start = new Date(job.started_at); const end = job.completed_at ? new Date(job.completed_at) : new Date(); const durationMs = end - start; // Convert to human-readable format const seconds = Math.floor(durationMs / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } }, /** * Start auto-refresh for active jobs */ startAutoRefresh() { // Clear any existing interval if (this.autoRefreshInterval) { clearInterval(this.autoRefreshInterval); } // Refresh every 10 seconds if there are active jobs this.autoRefreshInterval = setInterval(async () => { const hasActiveJobs = this.jobs.some(job => job.status === 'pending' || job.status === 'processing' ); if (hasActiveJobs) { adminMarketplaceLog.info('Auto-refreshing active jobs...'); await this.loadJobs(); } }, 10000); // 10 seconds }, /** * Stop auto-refresh (cleanup) */ stopAutoRefresh() { if (this.autoRefreshInterval) { clearInterval(this.autoRefreshInterval); this.autoRefreshInterval = null; } } }; } // Cleanup on page unload window.addEventListener('beforeunload', () => { if (window._adminMarketplaceInstance && window._adminMarketplaceInstance.stopAutoRefresh) { window._adminMarketplaceInstance.stopAutoRefresh(); } });