feat: add import error tracking and translation tabs

Import Error Tracking:
- Add MarketplaceImportError model to store detailed error information
- Store row number, identifier, error type, message, and row data for each error
- Add API endpoint GET /admin/marketplace-import-jobs/{job_id}/errors
- Add UI to view and browse import errors in job details modal
- Support pagination and error type filtering

Translation Tabs:
- Replace flat translation list with tabbed interface on product detail page
- Add language tabs with full language names
- Add copy-to-clipboard functionality for translation content
- Improved UX with better visual separation of translations

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-13 13:33:03 +01:00
parent 3316894c27
commit c2f42c2913
14 changed files with 542 additions and 44 deletions

View File

@@ -54,6 +54,12 @@ function adminImports() {
showJobModal: false,
selectedJob: null,
// Job errors state
jobErrors: [],
jobErrorsTotal: 0,
jobErrorsPage: 1,
loadingErrors: false,
// Auto-refresh for active jobs
autoRefreshInterval: null,
@@ -275,6 +281,58 @@ function adminImports() {
closeJobModal() {
this.showJobModal = false;
this.selectedJob = null;
// Clear errors state
this.jobErrors = [];
this.jobErrorsTotal = 0;
this.jobErrorsPage = 1;
},
/**
* Load errors for a specific job
*/
async loadJobErrors(jobId) {
if (!jobId) return;
this.loadingErrors = true;
this.jobErrorsPage = 1;
try {
const response = await apiClient.get(
`/admin/marketplace-import-jobs/${jobId}/errors?page=1&limit=20`
);
this.jobErrors = response.errors || [];
this.jobErrorsTotal = response.total || 0;
adminImportsLog.debug('Loaded job errors:', this.jobErrors.length);
} catch (error) {
adminImportsLog.error('Failed to load job errors:', error);
this.error = error.message || 'Failed to load import errors';
} finally {
this.loadingErrors = false;
}
},
/**
* Load more errors (pagination)
*/
async loadMoreJobErrors(jobId) {
if (!jobId || this.loadingErrors) return;
this.loadingErrors = true;
this.jobErrorsPage++;
try {
const response = await apiClient.get(
`/admin/marketplace-import-jobs/${jobId}/errors?page=${this.jobErrorsPage}&limit=20`
);
const newErrors = response.errors || [];
this.jobErrors = [...this.jobErrors, ...newErrors];
adminImportsLog.debug('Loaded more job errors:', newErrors.length);
} catch (error) {
adminImportsLog.error('Failed to load more job errors:', error);
this.jobErrorsPage--; // Revert page on failure
} finally {
this.loadingErrors = false;
}
},
/**

View File

@@ -176,6 +176,53 @@ function adminMarketplaceProductDetail() {
} catch (e) {
return dateString;
}
},
/**
* Get full language name from ISO code
*/
getLanguageName(code) {
const languages = {
'en': 'English',
'de': 'German',
'fr': 'French',
'lb': 'Luxembourgish',
'es': 'Spanish',
'it': 'Italian',
'nl': 'Dutch',
'pt': 'Portuguese',
'pl': 'Polish',
'cs': 'Czech',
'da': 'Danish',
'sv': 'Swedish',
'fi': 'Finnish',
'no': 'Norwegian',
'hu': 'Hungarian',
'ro': 'Romanian',
'bg': 'Bulgarian',
'el': 'Greek',
'sk': 'Slovak',
'sl': 'Slovenian',
'hr': 'Croatian',
'lt': 'Lithuanian',
'lv': 'Latvian',
'et': 'Estonian'
};
return languages[code?.toLowerCase()] || '';
},
/**
* Copy text to clipboard
*/
async copyToClipboard(text) {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
Utils.showToast('Copied to clipboard', 'success');
} catch (err) {
adminMarketplaceProductDetailLog.error('Failed to copy to clipboard:', err);
Utils.showToast('Failed to copy to clipboard', 'error');
}
}
};
}