Files
orion/static/admin/js/marketplace.js
Samir Boulahtit 6f8434f200 feat: add PlatformSettings for pagination and vendor filter improvements
Platform Settings:
- Add PlatformSettings utility in init-alpine.js with 5-min cache
- Add Display tab in /admin/settings for rows_per_page config
- Integrate PlatformSettings.getRowsPerPage() in all paginated pages
- Standardize default per_page to 20 across all admin pages
- Add documentation at docs/frontend/shared/platform-settings.md

Architecture Rules:
- Add JS-010: enforce PlatformSettings usage for pagination
- Add JS-011: enforce standard pagination structure
- Add JS-012: detect double /api/v1 prefix in apiClient calls
- Implement all rules in validate_architecture.py

Vendor Filter (Tom Select):
- Add vendor filter to marketplace-products, vendor-products,
  customers, inventory, and vendor-themes pages
- Add selectedVendor display panel with clear button
- Add localStorage persistence for vendor selection
- Fix double /api/v1 prefix in vendor-selector.js

Bug Fixes:
- Remove duplicate PlatformSettings from utils.js
- Fix customers.js pagination structure (page_size → per_page)
- Fix code-quality-violations.js pagination structure

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 22:39:34 +01:00

533 lines
17 KiB
JavaScript

// 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: 20,
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;
// Load platform settings for rows per page
if (window.PlatformSettings) {
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
}
// 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();
}
});