Files
orion/static/vendor/js/marketplace.js
Samir Boulahtit 36603178c3 feat: add email settings with database overrides for admin and vendor
Platform Email Settings (Admin):
- Add GET/PUT/DELETE /admin/settings/email/* endpoints
- Settings stored in admin_settings table override .env values
- Support all providers: SMTP, SendGrid, Mailgun, Amazon SES
- Edit mode UI with provider-specific configuration forms
- Reset to .env defaults functionality
- Test email to verify configuration

Vendor Email Settings:
- Add VendorEmailSettings model with one-to-one vendor relationship
- Migration: v0a1b2c3d4e5_add_vendor_email_settings.py
- Service: vendor_email_settings_service.py with tier validation
- API endpoints: /vendor/email-settings/* (CRUD, status, verify)
- Email tab in vendor settings page with full configuration
- Warning banner until email is configured (like billing warnings)
- Premium providers (SendGrid, Mailgun, SES) tier-gated to Business+

Email Service Updates:
- get_platform_email_config(db) checks DB first, then .env
- Configurable provider classes accept config dict
- EmailService uses database-aware providers
- Vendor emails use vendor's own SMTP (Wizamart doesn't pay)
- "Powered by Wizamart" footer for Essential/Professional tiers
- White-label (no footer) for Business/Enterprise tiers

Other:
- Add scripts/install.py for first-time platform setup
- Add make install target
- Update init-prod to include email template seeding

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 22:23:47 +01:00

344 lines
11 KiB
JavaScript

// static/vendor/js/marketplace.js
/**
* Vendor marketplace import page logic
*/
// ✅ Use centralized logger (with safe fallback)
const vendorMarketplaceLog = window.LogConfig.loggers.marketplace ||
window.LogConfig.createLogger('marketplace', false);
vendorMarketplaceLog.info('Loading...');
function vendorMarketplace() {
vendorMarketplaceLog.info('[VENDOR MARKETPLACE] vendorMarketplace() called');
return {
// ✅ Inherit base layout state
...data(),
// ✅ Set page identifier
currentPage: 'marketplace',
// Loading states
loading: false,
importing: false,
error: '',
successMessage: '',
// Import form
importForm: {
csv_url: '',
marketplace: 'Letzshop',
language: 'fr',
batch_size: 1000
},
// Vendor settings (for quick fill)
vendorSettings: {
letzshop_csv_url_fr: '',
letzshop_csv_url_en: '',
letzshop_csv_url_de: ''
},
// Import jobs
jobs: [],
totalJobs: 0,
page: 1,
limit: 10,
// Modal state
showJobModal: false,
selectedJob: null,
// Auto-refresh for active jobs
autoRefreshInterval: null,
async init() {
// Guard against multiple initialization
if (window._vendorMarketplaceInitialized) {
return;
}
window._vendorMarketplaceInitialized = true;
// IMPORTANT: Call parent init first to set vendorCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
await this.loadVendorSettings();
await this.loadJobs();
// Auto-refresh active jobs every 10 seconds
this.startAutoRefresh();
},
/**
* Load vendor settings (for quick fill)
*/
async loadVendorSettings() {
try {
const response = await apiClient.get('/vendor/settings');
this.vendorSettings = {
letzshop_csv_url_fr: response.letzshop_csv_url_fr || '',
letzshop_csv_url_en: response.letzshop_csv_url_en || '',
letzshop_csv_url_de: response.letzshop_csv_url_de || ''
};
} catch (error) {
vendorMarketplaceLog.error('[VENDOR MARKETPLACE] Failed to load vendor settings:', error);
// Non-critical, don't show error to user
}
},
/**
* Load import jobs
*/
async loadJobs() {
this.loading = true;
this.error = '';
try {
const response = await apiClient.get(
`/vendor/marketplace/imports?page=${this.page}&limit=${this.limit}`
);
this.jobs = response.items || [];
this.totalJobs = response.total || 0;
vendorMarketplaceLog.info('[VENDOR MARKETPLACE] Loaded jobs:', this.jobs.length);
} catch (error) {
vendorMarketplaceLog.error('[VENDOR MARKETPLACE] Failed to load jobs:', error);
this.error = error.message || 'Failed to load import jobs';
} finally {
this.loading = false;
}
},
/**
* Start new import
*/
async startImport() {
if (!this.importForm.csv_url) {
this.error = 'Please enter a CSV URL';
return;
}
this.importing = true;
this.error = '';
this.successMessage = '';
try {
const payload = {
source_url: this.importForm.csv_url,
marketplace: this.importForm.marketplace,
batch_size: this.importForm.batch_size
};
vendorMarketplaceLog.info('[VENDOR MARKETPLACE] Starting import:', payload);
const response = await apiClient.post('/vendor/marketplace/import', payload);
vendorMarketplaceLog.info('[VENDOR MARKETPLACE] Import started:', response);
this.successMessage = `Import job #${response.job_id} started successfully!`;
// Clear form
this.importForm.csv_url = '';
this.importForm.language = 'fr';
this.importForm.batch_size = 1000;
// Reload jobs to show the new import
await this.loadJobs();
// Clear success message after 5 seconds
setTimeout(() => {
this.successMessage = '';
}, 5000);
} catch (error) {
vendorMarketplaceLog.error('[VENDOR MARKETPLACE] Failed to start import:', error);
this.error = error.message || 'Failed to start import';
} finally {
this.importing = false;
}
},
/**
* Quick fill form with saved CSV URL
*/
quickFill(language) {
const urlMap = {
'fr': this.vendorSettings.letzshop_csv_url_fr,
'en': this.vendorSettings.letzshop_csv_url_en,
'de': this.vendorSettings.letzshop_csv_url_de
};
const url = urlMap[language];
if (url) {
this.importForm.csv_url = url;
this.importForm.language = language;
vendorMarketplaceLog.info('[VENDOR MARKETPLACE] Quick filled:', language, url);
}
},
/**
* Refresh jobs list
*/
async refreshJobs() {
await this.loadJobs();
},
/**
* Refresh single job status
*/
async refreshJobStatus(jobId) {
try {
const response = await apiClient.get(`/vendor/marketplace/imports/${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;
}
vendorMarketplaceLog.info('[VENDOR MARKETPLACE] Refreshed job:', jobId);
} catch (error) {
vendorMarketplaceLog.error('[VENDOR MARKETPLACE] Failed to refresh job:', error);
}
},
/**
* View job details in modal
*/
async viewJobDetails(jobId) {
try {
const response = await apiClient.get(`/vendor/marketplace/imports/${jobId}`);
this.selectedJob = response;
this.showJobModal = true;
vendorMarketplaceLog.info('[VENDOR MARKETPLACE] Viewing job details:', jobId);
} catch (error) {
vendorMarketplaceLog.error('[VENDOR MARKETPLACE] 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;
},
/**
* Pagination: Previous page
*/
async previousPage() {
if (this.page > 1) {
this.page--;
await this.loadJobs();
}
},
/**
* Pagination: Next page
*/
async nextPage() {
if (this.page * this.limit < this.totalJobs) {
this.page++;
await this.loadJobs();
}
},
/**
* Format date for display
*/
formatDate(dateString) {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
const locale = window.VENDOR_CONFIG?.locale || 'en-GB';
return date.toLocaleString(locale, {
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) {
vendorMarketplaceLog.info('[VENDOR MARKETPLACE] 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._vendorMarketplaceInstance && window._vendorMarketplaceInstance.stopAutoRefresh) {
window._vendorMarketplaceInstance.stopAutoRefresh();
}
});