feat: add logging, marketplace, and admin enhancements

Database & Migrations:
- Add application_logs table migration for hybrid cloud logging
- Add companies table migration and restructure vendor relationships

Logging System:
- Implement hybrid logging system (database + file)
- Add log_service for centralized log management
- Create admin logs page with filtering and viewing capabilities
- Add init_log_settings.py script for log configuration
- Enhance core logging with database integration

Marketplace Integration:
- Add marketplace admin page with product management
- Create marketplace vendor page with product listings
- Implement marketplace.js for both admin and vendor interfaces
- Add marketplace integration documentation

Admin Enhancements:
- Add imports management page and functionality
- Create settings page for admin configuration
- Add vendor themes management page
- Enhance vendor detail and edit pages
- Improve code quality dashboard and violation details
- Add logs viewing and management
- Update icons guide and shared icon system

Architecture & Documentation:
- Document frontend structure and component architecture
- Document models structure and relationships
- Add vendor-in-token architecture documentation
- Add vendor RBAC (role-based access control) documentation
- Document marketplace integration patterns
- Update architecture patterns documentation

Infrastructure:
- Add platform static files structure (css, img, js)
- Move architecture_scan.py to proper models location
- Update model imports and registrations
- Enhance exception handling
- Update dependency injection patterns

UI/UX:
- Improve vendor edit interface
- Update admin user interface
- Enhance page templates documentation
- Add vendor marketplace interface
This commit is contained in:
2025-12-01 21:51:07 +01:00
parent 915734e9b4
commit cc74970223
56 changed files with 8440 additions and 202 deletions

345
static/admin/js/imports.js Normal file
View File

@@ -0,0 +1,345 @@
// static/admin/js/imports.js
/**
* Admin platform monitoring - all import jobs
*/
// ✅ Use centralized logger
const adminImportsLog = window.LogConfig.loggers.imports;
console.log('[ADMIN IMPORTS] Loading...');
function adminImports() {
console.log('[ADMIN IMPORTS] adminImports() called');
return {
// ✅ Inherit base layout state
...data(),
// ✅ Set page identifier
currentPage: 'imports',
// Loading states
loading: false,
error: '',
// Vendors list
vendors: [],
// Stats
stats: {
total: 0,
active: 0,
completed: 0,
failed: 0
},
// Filters
filters: {
vendor_id: '',
status: '',
marketplace: '',
created_by: '' // 'me' or empty
},
// Import jobs
jobs: [],
totalJobs: 0,
page: 1,
limit: 20,
// Modal state
showJobModal: false,
selectedJob: null,
// Auto-refresh for active jobs
autoRefreshInterval: null,
async init() {
// Guard against multiple initialization
if (window._adminImportsInitialized) {
return;
}
window._adminImportsInitialized = true;
// IMPORTANT: Call parent init first
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
await this.loadVendors();
await this.loadJobs();
await this.loadStats();
// Auto-refresh active jobs every 15 seconds
this.startAutoRefresh();
},
/**
* Load all vendors for filtering
*/
async loadVendors() {
try {
const response = await apiClient.get('/admin/vendors?limit=1000');
this.vendors = response.vendors || [];
console.log('[ADMIN IMPORTS] Loaded vendors:', this.vendors.length);
} catch (error) {
console.error('[ADMIN IMPORTS] Failed to load vendors:', error);
}
},
/**
* Load statistics
*/
async loadStats() {
try {
const response = await apiClient.get('/admin/marketplace-import-jobs/stats');
this.stats = {
total: response.total || 0,
active: (response.pending || 0) + (response.processing || 0),
completed: response.completed || 0,
failed: response.failed || 0
};
console.log('[ADMIN IMPORTS] Loaded stats:', this.stats);
} catch (error) {
console.error('[ADMIN IMPORTS] Failed to load stats:', error);
// Non-critical, don't show error
}
},
/**
* Load ALL import jobs (with filters)
*/
async loadJobs() {
this.loading = true;
this.error = '';
try {
// Build query params
const params = new URLSearchParams({
page: this.page,
limit: this.limit
});
// Add filters
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);
}
if (this.filters.created_by === 'me') {
params.append('created_by_me', 'true');
}
const response = await apiClient.get(
`/admin/marketplace-import-jobs?${params.toString()}`
);
this.jobs = response.items || [];
this.totalJobs = response.total || 0;
console.log('[ADMIN IMPORTS] Loaded all jobs:', this.jobs.length);
} catch (error) {
console.error('[ADMIN IMPORTS] Failed to load jobs:', error);
this.error = error.message || 'Failed to load import jobs';
} finally {
this.loading = false;
}
},
/**
* Apply filters and reload
*/
async applyFilters() {
this.page = 1; // Reset to first page when filtering
await this.loadJobs();
await this.loadStats(); // Update stats based on filters
},
/**
* Clear all filters and reload
*/
async clearFilters() {
this.filters.vendor_id = '';
this.filters.status = '';
this.filters.marketplace = '';
this.filters.created_by = '';
this.page = 1;
await this.loadJobs();
await this.loadStats();
},
/**
* Refresh jobs list
*/
async refreshJobs() {
await this.loadJobs();
await this.loadStats();
},
/**
* 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;
}
console.log('[ADMIN IMPORTS] Refreshed job:', jobId);
} catch (error) {
console.error('[ADMIN IMPORTS] 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;
console.log('[ADMIN IMPORTS] Viewing job details:', jobId);
} catch (error) {
console.error('[ADMIN IMPORTS] 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
*/
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);
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 15 seconds if there are active jobs
this.autoRefreshInterval = setInterval(async () => {
const hasActiveJobs = this.jobs.some(job =>
job.status === 'pending' || job.status === 'processing'
);
if (hasActiveJobs) {
console.log('[ADMIN IMPORTS] Auto-refreshing active jobs...');
await this.loadJobs();
await this.loadStats();
}
}, 15000); // 15 seconds
},
/**
* Stop auto-refresh (cleanup)
*/
stopAutoRefresh() {
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval);
this.autoRefreshInterval = null;
}
}
};
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
if (window._adminImportsInstance && window._adminImportsInstance.stopAutoRefresh) {
window._adminImportsInstance.stopAutoRefresh();
}
});

173
static/admin/js/logs.js Normal file
View File

@@ -0,0 +1,173 @@
// static/admin/js/logs.js
const logsLog = window.LogConfig?.loggers?.logs || console;
function adminLogs() {
// Get base data
const baseData = typeof data === 'function' ? data() : {};
return {
// Inherit base layout functionality from init-alpine.js
...baseData,
// Logs-specific state
currentPage: 'logs',
loading: true,
error: null,
successMessage: null,
logSource: 'database',
logs: [],
totalLogs: 0,
stats: {
total_count: 0,
warning_count: 0,
error_count: 0,
critical_count: 0
},
selectedLog: null,
filters: {
level: '',
module: '',
search: '',
skip: 0,
limit: 50
},
logFiles: [],
selectedFile: '',
fileContent: null,
async init() {
logsLog.info('=== LOGS PAGE INITIALIZING ===');
await this.loadStats();
await this.loadLogs();
},
async refresh() {
this.error = null;
this.successMessage = null;
await this.loadStats();
if (this.logSource === 'database') {
await this.loadLogs();
} else {
await this.loadFileLogs();
}
},
async loadStats() {
try {
const data = await apiClient.get('/admin/logs/statistics?days=7');
this.stats = data;
logsLog.info('Log statistics loaded:', this.stats);
} catch (error) {
logsLog.error('Failed to load log statistics:', error);
}
},
async loadLogs() {
this.loading = true;
this.error = null;
try {
const params = new URLSearchParams();
if (this.filters.level) params.append('level', this.filters.level);
if (this.filters.module) params.append('module', this.filters.module);
if (this.filters.search) params.append('search', this.filters.search);
params.append('skip', this.filters.skip);
params.append('limit', this.filters.limit);
const data = await apiClient.get(`/admin/logs/database?${params}`);
this.logs = data.logs;
this.totalLogs = data.total;
logsLog.info(`Loaded ${this.logs.length} logs (total: ${this.totalLogs})`);
} catch (error) {
logsLog.error('Failed to load logs:', error);
this.error = error.response?.data?.detail || 'Failed to load logs';
} finally {
this.loading = false;
}
},
async loadFileLogs() {
this.loading = true;
this.error = null;
try {
const data = await apiClient.get('/admin/logs/files');
this.logFiles = data.files;
if (this.logFiles.length > 0 && !this.selectedFile) {
this.selectedFile = this.logFiles[0].filename;
await this.loadFileContent();
}
logsLog.info(`Loaded ${this.logFiles.length} log files`);
} catch (error) {
logsLog.error('Failed to load log files:', error);
this.error = error.response?.data?.detail || 'Failed to load log files';
} finally {
this.loading = false;
}
},
async loadFileContent() {
if (!this.selectedFile) return;
this.loading = true;
this.error = null;
try {
const data = await apiClient.get(`/admin/logs/files/${this.selectedFile}?lines=500`);
this.fileContent = data;
logsLog.info(`Loaded file content for ${this.selectedFile}`);
} catch (error) {
logsLog.error('Failed to load file content:', error);
this.error = error.response?.data?.detail || 'Failed to load file content';
} finally {
this.loading = false;
}
},
async downloadLogFile() {
if (!this.selectedFile) return;
try {
const token = localStorage.getItem('admin_token');
// Note: window.open bypasses apiClient, so we need the full path
window.open(`/api/v1/admin/logs/files/${this.selectedFile}/download?token=${token}`, '_blank');
} catch (error) {
logsLog.error('Failed to download log file:', error);
this.error = 'Failed to download log file';
}
},
resetFilters() {
this.filters = {
level: '',
module: '',
search: '',
skip: 0,
limit: 50
};
this.loadLogs();
},
nextPage() {
this.filters.skip += this.filters.limit;
this.loadLogs();
},
previousPage() {
this.filters.skip = Math.max(0, this.filters.skip - this.filters.limit);
this.loadLogs();
},
showLogDetail(log) {
this.selectedLog = log;
},
formatTimestamp(timestamp) {
return new Date(timestamp).toLocaleString();
}
};
}
logsLog.info('Logs module loaded');

View File

@@ -0,0 +1,429 @@
// static/admin/js/marketplace.js
/**
* Admin marketplace import page logic
*/
// ✅ Use centralized logger
const adminMarketplaceLog = window.LogConfig.loggers.marketplace;
console.log('[ADMIN MARKETPLACE] Loading...');
function adminMarketplace() {
console.log('[ADMIN MARKETPLACE] adminMarketplace() called');
return {
// ✅ Inherit base layout state
...data(),
// ✅ Set page identifier
currentPage: 'marketplace',
// Loading states
loading: false,
importing: false,
error: '',
successMessage: '',
// 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: [],
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._adminMarketplaceInitialized) {
return;
}
window._adminMarketplaceInitialized = true;
// IMPORTANT: Call parent init first
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
await this.loadVendors();
await this.loadJobs();
// Auto-refresh active jobs every 10 seconds
this.startAutoRefresh();
},
/**
* Load all vendors for dropdown
*/
async loadVendors() {
try {
const response = await apiClient.get('/admin/vendors?limit=1000');
this.vendors = response.vendors || [];
console.log('[ADMIN MARKETPLACE] Loaded vendors:', this.vendors.length);
} catch (error) {
console.error('[ADMIN MARKETPLACE] 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;
console.log('[ADMIN MARKETPLACE] 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;
console.log('[ADMIN MARKETPLACE] Auto-populated CSV URL:', this.importForm.language, url);
} else {
console.log('[ADMIN MARKETPLACE] 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({
page: this.page,
limit: this.limit,
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.totalJobs = response.total || 0;
console.log('[ADMIN MARKETPLACE] Loaded my jobs:', this.jobs.length);
} catch (error) {
console.error('[ADMIN MARKETPLACE] 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
};
console.log('[ADMIN MARKETPLACE] Starting import:', payload);
const response = await apiClient.post('/admin/marketplace-import-jobs', payload);
console.log('[ADMIN MARKETPLACE] 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) {
console.error('[ADMIN 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 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;
console.log('[ADMIN MARKETPLACE] Quick filled:', language, url);
}
},
/**
* Clear all filters and reload
*/
clearFilters() {
this.filters.vendor_id = '';
this.filters.status = '';
this.filters.marketplace = '';
this.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;
}
console.log('[ADMIN MARKETPLACE] Refreshed job:', jobId);
} catch (error) {
console.error('[ADMIN MARKETPLACE] 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;
console.log('[ADMIN MARKETPLACE] Viewing job details:', jobId);
} catch (error) {
console.error('[ADMIN 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;
},
/**
* 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
*/
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);
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) {
console.log('[ADMIN 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._adminMarketplaceInstance && window._adminMarketplaceInstance.stopAutoRefresh) {
window._adminMarketplaceInstance.stopAutoRefresh();
}
});

112
static/admin/js/settings.js Normal file
View File

@@ -0,0 +1,112 @@
// static/admin/js/settings.js
const settingsLog = window.LogConfig?.loggers?.settings || console;
function adminSettings() {
// Get base data
const baseData = typeof data === 'function' ? data() : {};
return {
// Inherit base layout functionality from init-alpine.js
...baseData,
// Settings-specific state
currentPage: 'settings',
loading: true,
saving: false,
error: null,
successMessage: null,
activeTab: 'logging',
logSettings: {
log_level: 'INFO',
log_file_max_size_mb: 10,
log_file_backup_count: 5,
db_log_retention_days: 30,
file_logging_enabled: true,
db_logging_enabled: true
},
async init() {
try {
settingsLog.info('=== SETTINGS PAGE INITIALIZING ===');
await this.loadLogSettings();
} catch (error) {
console.error('[Settings] Init failed:', error);
this.error = 'Failed to initialize settings page';
}
},
async refresh() {
this.error = null;
this.successMessage = null;
await this.loadLogSettings();
},
async loadLogSettings() {
this.loading = true;
this.error = null;
try {
const data = await apiClient.get('/admin/logs/settings');
this.logSettings = data;
settingsLog.info('Log settings loaded:', this.logSettings);
} catch (error) {
settingsLog.error('Failed to load log settings:', error);
this.error = error.response?.data?.detail || 'Failed to load log settings';
} finally {
this.loading = false;
}
},
async saveLogSettings() {
this.saving = true;
this.error = null;
this.successMessage = null;
try {
const data = await apiClient.put('/admin/logs/settings', this.logSettings);
this.successMessage = data.message || 'Log settings saved successfully';
// Auto-hide success message after 5 seconds
setTimeout(() => {
this.successMessage = null;
}, 5000);
settingsLog.info('Log settings saved successfully');
} catch (error) {
settingsLog.error('Failed to save log settings:', error);
this.error = error.response?.data?.detail || 'Failed to save log settings';
} finally {
this.saving = false;
}
},
async cleanupOldLogs() {
if (!confirm(`This will delete all logs older than ${this.logSettings.db_log_retention_days} days. Continue?`)) {
return;
}
this.error = null;
this.successMessage = null;
try {
const data = await apiClient.delete(
`/admin/logs/database/cleanup?retention_days=${this.logSettings.db_log_retention_days}&confirm=true`
);
this.successMessage = data.message || 'Old logs cleaned up successfully';
// Auto-hide success message after 5 seconds
setTimeout(() => {
this.successMessage = null;
}, 5000);
settingsLog.info('Old logs cleaned up successfully');
} catch (error) {
settingsLog.error('Failed to cleanup logs:', error);
this.error = error.response?.data?.detail || 'Failed to cleanup old logs';
}
}
};
}
settingsLog.info('Settings module loaded');

View File

@@ -73,7 +73,10 @@ function adminVendorEdit() {
contact_phone: response.contact_phone || '',
website: response.website || '',
business_address: response.business_address || '',
tax_number: response.tax_number || ''
tax_number: response.tax_number || '',
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 || ''
};
editLog.info(`Vendor loaded in ${duration}ms`, {

View File

@@ -0,0 +1,65 @@
// static/admin/js/vendor-themes.js
/**
* Admin vendor themes selection page
*/
console.log('[ADMIN VENDOR THEMES] Loading...');
function adminVendorThemes() {
console.log('[ADMIN VENDOR THEMES] adminVendorThemes() called');
return {
// Inherit base layout state
...data(),
// Set page identifier
currentPage: 'vendor-theme',
// State
loading: false,
error: '',
vendors: [],
selectedVendorCode: '',
async init() {
// Guard against multiple initialization
if (window._adminVendorThemesInitialized) {
return;
}
window._adminVendorThemesInitialized = true;
// Call parent init first
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
await this.loadVendors();
},
async loadVendors() {
this.loading = true;
this.error = '';
try {
const response = await apiClient.get('/admin/vendors?limit=1000');
this.vendors = response.vendors || [];
console.log('[ADMIN VENDOR THEMES] Loaded vendors:', this.vendors.length);
} catch (error) {
console.error('[ADMIN VENDOR THEMES] Failed to load vendors:', error);
this.error = error.message || 'Failed to load vendors';
} finally {
this.loading = false;
}
},
navigateToTheme() {
if (!this.selectedVendorCode) {
return;
}
window.location.href = `/admin/vendors/${this.selectedVendorCode}/theme`;
}
};
}
console.log('[ADMIN VENDOR THEMES] Module loaded');

View File

View File

View File

View File

@@ -24,7 +24,8 @@ const Icons = {
'user-group': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>`,
'identification': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2"/></svg>`,
'badge-check': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"/></svg>`,
'shield-check': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>`,
// Actions
'edit': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>`,
'delete': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>`,
@@ -44,6 +45,7 @@ const Icons = {
'shopping-cart': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/></svg>`,
'credit-card': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/></svg>`,
'currency-dollar': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,
'currency-euro': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.121 15.536c-1.171 1.952-3.07 1.952-4.242 0-1.172-1.953-1.172-5.119 0-7.072 1.171-1.952 3.07-1.952 4.242 0M8 10.5h4m-4 3h4m9-1.5a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,
'gift': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"/></svg>`,
'tag': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>`,
'truck': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 104 0m-4 0a2 2 0 114 0m6 0a2 2 0 104 0m-4 0a2 2 0 114 0"/></svg>`,
@@ -65,6 +67,7 @@ const Icons = {
// Files & Documents
'document': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>`,
'document-text': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>`,
'folder': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>`,
'folder-open': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z"/></svg>`,
'download': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>`,

341
static/vendor/js/marketplace.js vendored Normal file
View File

@@ -0,0 +1,341 @@
// static/vendor/js/marketplace.js
/**
* Vendor marketplace import page logic
*/
// ✅ Use centralized logger
const vendorMarketplaceLog = window.LogConfig.loggers.marketplace;
console.log('[VENDOR MARKETPLACE] Loading...');
function vendorMarketplace() {
console.log('[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) {
console.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;
console.log('[VENDOR MARKETPLACE] Loaded jobs:', this.jobs.length);
} catch (error) {
console.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
};
console.log('[VENDOR MARKETPLACE] Starting import:', payload);
const response = await apiClient.post('/vendor/marketplace/import', payload);
console.log('[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) {
console.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;
console.log('[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;
}
console.log('[VENDOR MARKETPLACE] Refreshed job:', jobId);
} catch (error) {
console.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;
console.log('[VENDOR MARKETPLACE] Viewing job details:', jobId);
} catch (error) {
console.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);
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) {
console.log('[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();
}
});