Consolidate all tab-specific pagination into a unified pagination object and use the shared pagination macro for consistent look and feel across Orders, Products, Jobs, and Exceptions tabs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1604 lines
62 KiB
JavaScript
1604 lines
62 KiB
JavaScript
// static/admin/js/marketplace-letzshop.js
|
|
/**
|
|
* Admin marketplace Letzshop management page logic
|
|
* Unified page for Products (Import/Export), Orders, and Settings
|
|
*/
|
|
|
|
// Use centralized logger
|
|
const marketplaceLetzshopLog = window.LogConfig.createLogger('MARKETPLACE-LETZSHOP');
|
|
|
|
marketplaceLetzshopLog.info('Loading...');
|
|
|
|
function adminMarketplaceLetzshop() {
|
|
marketplaceLetzshopLog.info('adminMarketplaceLetzshop() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'marketplace-letzshop',
|
|
|
|
// Tab state
|
|
activeTab: 'products',
|
|
|
|
// Loading states
|
|
loading: false,
|
|
importing: false,
|
|
exporting: false,
|
|
importingOrders: false,
|
|
importingHistorical: false,
|
|
loadingOrders: false,
|
|
loadingJobs: false,
|
|
savingCredentials: false,
|
|
savingCsvUrls: false,
|
|
testingConnection: false,
|
|
submittingTracking: false,
|
|
|
|
// Historical import state
|
|
historicalImportResult: null,
|
|
historicalImportJobId: null,
|
|
historicalImportProgress: null,
|
|
historicalImportPollInterval: null,
|
|
|
|
// Messages
|
|
error: '',
|
|
successMessage: '',
|
|
|
|
// Tom Select instance
|
|
tomSelectInstance: null,
|
|
|
|
// Selected vendor
|
|
selectedVendor: null,
|
|
|
|
// Letzshop status for selected vendor
|
|
letzshopStatus: {
|
|
is_configured: false,
|
|
auto_sync_enabled: false,
|
|
last_sync_at: null,
|
|
last_sync_status: null
|
|
},
|
|
|
|
// Credentials
|
|
credentials: null,
|
|
showApiKey: false,
|
|
|
|
// Import form
|
|
importForm: {
|
|
csv_url: '',
|
|
language: 'fr',
|
|
batch_size: 1000
|
|
},
|
|
|
|
// Export settings
|
|
exportLanguage: 'fr',
|
|
exportIncludeInactive: false,
|
|
|
|
// Settings form
|
|
settingsForm: {
|
|
api_key: '',
|
|
auto_sync_enabled: false,
|
|
sync_interval_minutes: 15,
|
|
test_mode_enabled: false,
|
|
letzshop_csv_url_fr: '',
|
|
letzshop_csv_url_en: '',
|
|
letzshop_csv_url_de: '',
|
|
default_carrier: '',
|
|
carrier_greco_label_url: 'https://dispatchweb.fr/Tracky/Home/',
|
|
carrier_colissimo_label_url: '',
|
|
carrier_xpresslogistics_label_url: ''
|
|
},
|
|
savingCarrierSettings: false,
|
|
|
|
// Unified pagination (shared across tabs, updated based on activeTab)
|
|
pagination: {
|
|
page: 1,
|
|
per_page: 20,
|
|
total: 0,
|
|
pages: 0
|
|
},
|
|
|
|
// Orders
|
|
orders: [],
|
|
ordersFilter: '',
|
|
ordersSearch: '',
|
|
ordersHasDeclinedItems: false,
|
|
orderStats: { pending: 0, processing: 0, shipped: 0, delivered: 0, cancelled: 0, total: 0, has_declined_items: 0 },
|
|
|
|
// Exceptions
|
|
exceptions: [],
|
|
exceptionsFilter: '',
|
|
exceptionsSearch: '',
|
|
exceptionStats: { pending: 0, resolved: 0, ignored: 0, total: 0, orders_with_exceptions: 0 },
|
|
loadingExceptions: false,
|
|
|
|
// Jobs
|
|
jobs: [],
|
|
jobsFilter: { type: '', status: '' },
|
|
|
|
// Products Tab
|
|
products: [],
|
|
loadingProducts: false,
|
|
productFilters: { search: '', is_active: '' },
|
|
productStats: { total: 0, active: 0, inactive: 0, last_sync: null },
|
|
showImportModal: false,
|
|
|
|
// Modals
|
|
showTrackingModal: false,
|
|
showOrderModal: false,
|
|
showResolveModal: false,
|
|
showJobDetailsModal: false,
|
|
selectedOrder: null,
|
|
selectedJobDetails: null,
|
|
selectedExceptionForResolve: null,
|
|
trackingForm: { tracking_number: '', tracking_provider: '' },
|
|
resolveForm: { product_id: null, product_name: '', notes: '', bulk_resolve: false },
|
|
productSearchQuery: '',
|
|
productSearchResults: [],
|
|
searchingProducts: false,
|
|
submittingResolve: false,
|
|
|
|
// Computed: Total pages
|
|
get totalPages() {
|
|
return this.pagination.pages || Math.ceil(this.pagination.total / this.pagination.per_page) || 0;
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
|
|
// Pagination: Previous page
|
|
previousPage() {
|
|
if (this.pagination.page > 1) {
|
|
this.pagination.page--;
|
|
this._loadCurrentTabData();
|
|
}
|
|
},
|
|
|
|
// Pagination: Next page
|
|
nextPage() {
|
|
if (this.pagination.page < this.totalPages) {
|
|
this.pagination.page++;
|
|
this._loadCurrentTabData();
|
|
}
|
|
},
|
|
|
|
// Pagination: Go to specific page
|
|
goToPage(pageNum) {
|
|
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
|
|
this.pagination.page = pageNum;
|
|
this._loadCurrentTabData();
|
|
}
|
|
},
|
|
|
|
// Helper: Load data for current active tab
|
|
_loadCurrentTabData() {
|
|
switch (this.activeTab) {
|
|
case 'orders':
|
|
this.loadOrders();
|
|
break;
|
|
case 'products':
|
|
this.loadProducts();
|
|
break;
|
|
case 'jobs':
|
|
this.loadJobs();
|
|
break;
|
|
case 'exceptions':
|
|
this.loadExceptions();
|
|
break;
|
|
}
|
|
},
|
|
|
|
async init() {
|
|
marketplaceLetzshopLog.info('init() called');
|
|
|
|
// Guard against multiple initialization
|
|
if (window._marketplaceLetzshopInitialized) {
|
|
marketplaceLetzshopLog.warn('Already initialized, skipping');
|
|
return;
|
|
}
|
|
window._marketplaceLetzshopInitialized = true;
|
|
|
|
// Load platform settings for pagination
|
|
if (window.PlatformSettings) {
|
|
const rowsPerPage = await window.PlatformSettings.getRowsPerPage();
|
|
this.pagination.per_page = rowsPerPage;
|
|
marketplaceLetzshopLog.info('Loaded rows per page setting:', rowsPerPage);
|
|
}
|
|
|
|
// Initialize Tom Select after a short delay to ensure DOM is ready
|
|
this.$nextTick(() => {
|
|
this.initTomSelect();
|
|
});
|
|
|
|
// Watch for tab changes to reload relevant data
|
|
this.$watch('activeTab', async (newTab) => {
|
|
marketplaceLetzshopLog.info('Tab changed to:', newTab);
|
|
// Reset pagination to page 1 when switching tabs
|
|
this.pagination.page = 1;
|
|
this.pagination.total = 0;
|
|
this.pagination.pages = 0;
|
|
|
|
if (newTab === 'jobs') {
|
|
await this.loadJobs();
|
|
} else if (newTab === 'products') {
|
|
await this.loadProducts();
|
|
} else if (newTab === 'orders') {
|
|
await this.loadOrders();
|
|
} else if (newTab === 'exceptions') {
|
|
await Promise.all([this.loadExceptions(), this.loadExceptionStats()]);
|
|
}
|
|
});
|
|
|
|
// Check localStorage for last selected vendor
|
|
const savedVendorId = localStorage.getItem('letzshop_selected_vendor_id');
|
|
if (savedVendorId) {
|
|
marketplaceLetzshopLog.info('Restoring saved vendor:', savedVendorId);
|
|
// Load saved vendor after TomSelect is ready
|
|
setTimeout(async () => {
|
|
await this.restoreSavedVendor(parseInt(savedVendorId));
|
|
}, 200);
|
|
} else {
|
|
// Load cross-vendor data when no vendor selected
|
|
await this.loadCrossVendorData();
|
|
}
|
|
|
|
marketplaceLetzshopLog.info('Initialization complete');
|
|
},
|
|
|
|
/**
|
|
* Restore previously selected vendor from localStorage
|
|
*/
|
|
async restoreSavedVendor(vendorId) {
|
|
try {
|
|
// Load vendor details first
|
|
const vendor = await apiClient.get(`/admin/vendors/${vendorId}`);
|
|
|
|
// Add to TomSelect and select (silent to avoid double-triggering)
|
|
if (this.tomSelectInstance) {
|
|
this.tomSelectInstance.addOption({
|
|
id: vendor.id,
|
|
name: vendor.name,
|
|
vendor_code: vendor.vendor_code
|
|
});
|
|
this.tomSelectInstance.setValue(vendor.id, true);
|
|
}
|
|
|
|
// Manually call selectVendor since we used silent mode above
|
|
// This sets selectedVendor and loads all vendor-specific data
|
|
await this.selectVendor(vendor.id);
|
|
|
|
marketplaceLetzshopLog.info('Restored saved vendor:', vendor.name);
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to restore saved vendor:', error);
|
|
// Clear invalid saved vendor
|
|
localStorage.removeItem('letzshop_selected_vendor_id');
|
|
// Load cross-vendor data instead
|
|
await this.loadCrossVendorData();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load cross-vendor aggregate data (when no vendor is selected)
|
|
*/
|
|
async loadCrossVendorData() {
|
|
marketplaceLetzshopLog.info('Loading cross-vendor data');
|
|
this.loading = true;
|
|
|
|
try {
|
|
await Promise.all([
|
|
this.loadProducts(),
|
|
this.loadOrders(),
|
|
this.loadExceptions(),
|
|
this.loadExceptionStats(),
|
|
this.loadJobs()
|
|
]);
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to load cross-vendor data:', error);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Initialize Tom Select for vendor autocomplete
|
|
*/
|
|
initTomSelect() {
|
|
const selectEl = this.$refs.vendorSelect;
|
|
if (!selectEl) {
|
|
marketplaceLetzshopLog.error('Vendor select element not found');
|
|
return;
|
|
}
|
|
|
|
// Wait for TomSelect to be available
|
|
if (typeof TomSelect === 'undefined') {
|
|
marketplaceLetzshopLog.warn('TomSelect not loaded yet, retrying...');
|
|
setTimeout(() => this.initTomSelect(), 100);
|
|
return;
|
|
}
|
|
|
|
marketplaceLetzshopLog.info('Initializing Tom Select');
|
|
|
|
this.tomSelectInstance = new TomSelect(selectEl, {
|
|
valueField: 'id',
|
|
labelField: 'name',
|
|
searchField: ['name', 'vendor_code'],
|
|
maxOptions: 50,
|
|
placeholder: 'Search vendor by name or code...',
|
|
load: async (query, callback) => {
|
|
if (query.length < 2) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
try {
|
|
const response = await apiClient.get(`/admin/vendors?search=${encodeURIComponent(query)}&limit=50`);
|
|
const vendors = response.vendors.map(v => ({
|
|
id: v.id,
|
|
name: v.name,
|
|
vendor_code: v.vendor_code
|
|
}));
|
|
callback(vendors);
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to search vendors:', error);
|
|
callback([]);
|
|
}
|
|
},
|
|
render: {
|
|
option: (data, escape) => {
|
|
return `<div class="flex justify-between items-center">
|
|
<span>${escape(data.name)}</span>
|
|
<span class="text-xs text-gray-400 ml-2">${escape(data.vendor_code)}</span>
|
|
</div>`;
|
|
},
|
|
item: (data, escape) => {
|
|
return `<div>${escape(data.name)} <span class="text-gray-400">(${escape(data.vendor_code)})</span></div>`;
|
|
}
|
|
},
|
|
onChange: async (value) => {
|
|
if (value) {
|
|
await this.selectVendor(parseInt(value));
|
|
} else {
|
|
this.clearVendorSelection();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Handle vendor selection
|
|
*/
|
|
async selectVendor(vendorId) {
|
|
marketplaceLetzshopLog.info('Selecting vendor:', vendorId);
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
// Load vendor details
|
|
const vendor = await apiClient.get(`/admin/vendors/${vendorId}`);
|
|
this.selectedVendor = vendor;
|
|
|
|
// Save to localStorage for persistence
|
|
localStorage.setItem('letzshop_selected_vendor_id', vendorId.toString());
|
|
|
|
// Pre-fill settings form with CSV URLs
|
|
this.settingsForm.letzshop_csv_url_fr = vendor.letzshop_csv_url_fr || '';
|
|
this.settingsForm.letzshop_csv_url_en = vendor.letzshop_csv_url_en || '';
|
|
this.settingsForm.letzshop_csv_url_de = vendor.letzshop_csv_url_de || '';
|
|
|
|
// Load Letzshop status and credentials
|
|
await this.loadLetzshopStatus();
|
|
|
|
// Load orders, exceptions, products, and jobs
|
|
await Promise.all([
|
|
this.loadOrders(),
|
|
this.loadExceptions(),
|
|
this.loadExceptionStats(),
|
|
this.loadProducts(),
|
|
this.loadJobs()
|
|
]);
|
|
|
|
marketplaceLetzshopLog.info('Vendor loaded:', vendor.name);
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to load vendor:', error);
|
|
this.error = error.message || 'Failed to load vendor';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear vendor selection
|
|
*/
|
|
async clearVendorSelection() {
|
|
// Clear TomSelect dropdown
|
|
if (this.tomSelectInstance) {
|
|
this.tomSelectInstance.clear();
|
|
}
|
|
|
|
this.selectedVendor = null;
|
|
this.letzshopStatus = { is_configured: false };
|
|
this.credentials = null;
|
|
this.ordersFilter = '';
|
|
this.ordersSearch = '';
|
|
this.ordersHasDeclinedItems = false;
|
|
this.exceptionsFilter = '';
|
|
this.exceptionsSearch = '';
|
|
this.settingsForm = {
|
|
api_key: '',
|
|
auto_sync_enabled: false,
|
|
sync_interval_minutes: 15,
|
|
test_mode_enabled: false,
|
|
letzshop_csv_url_fr: '',
|
|
letzshop_csv_url_en: '',
|
|
letzshop_csv_url_de: '',
|
|
default_carrier: '',
|
|
carrier_greco_label_url: 'https://dispatchweb.fr/Tracky/Home/',
|
|
carrier_colissimo_label_url: '',
|
|
carrier_xpresslogistics_label_url: ''
|
|
};
|
|
|
|
// Clear localStorage
|
|
localStorage.removeItem('letzshop_selected_vendor_id');
|
|
|
|
// Load cross-vendor data
|
|
await this.loadCrossVendorData();
|
|
},
|
|
|
|
/**
|
|
* Load Letzshop status and credentials for selected vendor
|
|
*/
|
|
async loadLetzshopStatus() {
|
|
if (!this.selectedVendor) return;
|
|
|
|
try {
|
|
const response = await apiClient.get(`/admin/letzshop/vendors/${this.selectedVendor.id}/credentials`);
|
|
this.credentials = response;
|
|
this.letzshopStatus = {
|
|
is_configured: true,
|
|
auto_sync_enabled: response.auto_sync_enabled,
|
|
last_sync_at: response.last_sync_at,
|
|
last_sync_status: response.last_sync_status
|
|
};
|
|
this.settingsForm.auto_sync_enabled = response.auto_sync_enabled;
|
|
this.settingsForm.sync_interval_minutes = response.sync_interval_minutes || 15;
|
|
this.settingsForm.test_mode_enabled = response.test_mode_enabled || false;
|
|
this.settingsForm.default_carrier = response.default_carrier || '';
|
|
this.settingsForm.carrier_greco_label_url = response.carrier_greco_label_url || 'https://dispatchweb.fr/Tracky/Home/';
|
|
this.settingsForm.carrier_colissimo_label_url = response.carrier_colissimo_label_url || '';
|
|
this.settingsForm.carrier_xpresslogistics_label_url = response.carrier_xpresslogistics_label_url || '';
|
|
} catch (error) {
|
|
if (error.status === 404) {
|
|
// Not configured
|
|
this.letzshopStatus = { is_configured: false };
|
|
this.credentials = null;
|
|
} else {
|
|
marketplaceLetzshopLog.error('Failed to load Letzshop status:', error);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Refresh all data for selected vendor
|
|
*/
|
|
async refreshData() {
|
|
if (!this.selectedVendor) return;
|
|
await this.selectVendor(this.selectedVendor.id);
|
|
},
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// PRODUCTS TAB - LISTING (Letzshop marketplace products)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Load Letzshop products
|
|
* When vendor is selected: shows products for that vendor
|
|
* When no vendor selected: shows ALL Letzshop marketplace products
|
|
*/
|
|
async loadProducts() {
|
|
this.loadingProducts = true;
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
marketplace: 'Letzshop',
|
|
skip: ((this.pagination.page - 1) * this.pagination.per_page).toString(),
|
|
limit: this.pagination.per_page.toString()
|
|
});
|
|
|
|
// Filter by vendor if one is selected
|
|
if (this.selectedVendor) {
|
|
params.append('vendor_name', this.selectedVendor.name);
|
|
}
|
|
|
|
if (this.productFilters.search) {
|
|
params.append('search', this.productFilters.search);
|
|
}
|
|
|
|
if (this.productFilters.is_active !== '') {
|
|
params.append('is_active', this.productFilters.is_active);
|
|
}
|
|
|
|
const response = await apiClient.get(`/admin/products?${params}`);
|
|
this.products = response.products || [];
|
|
this.pagination.total = response.total || 0;
|
|
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
|
|
|
|
// Load stats separately
|
|
await this.loadProductStats();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to load products:', error);
|
|
this.products = [];
|
|
this.pagination.total = 0;
|
|
} finally {
|
|
this.loadingProducts = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load product statistics for Letzshop products
|
|
* Shows stats for selected vendor or all Letzshop products
|
|
*/
|
|
async loadProductStats() {
|
|
try {
|
|
const params = new URLSearchParams({
|
|
marketplace: 'Letzshop'
|
|
});
|
|
|
|
// Filter by vendor if one is selected
|
|
if (this.selectedVendor) {
|
|
params.append('vendor_name', this.selectedVendor.name);
|
|
}
|
|
|
|
const response = await apiClient.get(`/admin/products/stats?${params}`);
|
|
this.productStats = {
|
|
total: response.total || 0,
|
|
active: response.active || 0,
|
|
inactive: response.inactive || 0,
|
|
last_sync: null // TODO: Get from last import job
|
|
};
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to load product stats:', error);
|
|
}
|
|
},
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// PRODUCTS TAB - IMPORT
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Import all languages from configured CSV URLs
|
|
*/
|
|
async startImportAllLanguages() {
|
|
if (!this.selectedVendor) return;
|
|
|
|
this.importing = true;
|
|
this.error = '';
|
|
this.successMessage = '';
|
|
this.showImportModal = false;
|
|
|
|
try {
|
|
const languages = [];
|
|
if (this.selectedVendor.letzshop_csv_url_fr) languages.push({ url: this.selectedVendor.letzshop_csv_url_fr, lang: 'fr' });
|
|
if (this.selectedVendor.letzshop_csv_url_en) languages.push({ url: this.selectedVendor.letzshop_csv_url_en, lang: 'en' });
|
|
if (this.selectedVendor.letzshop_csv_url_de) languages.push({ url: this.selectedVendor.letzshop_csv_url_de, lang: 'de' });
|
|
|
|
if (languages.length === 0) {
|
|
this.error = 'No CSV URLs configured. Please set them in Settings.';
|
|
this.importing = false;
|
|
return;
|
|
}
|
|
|
|
// Start import jobs for all languages
|
|
for (const { url, lang } of languages) {
|
|
await apiClient.post('/admin/marketplace-import-jobs', {
|
|
vendor_id: this.selectedVendor.id,
|
|
source_url: url,
|
|
marketplace: 'Letzshop',
|
|
language: lang,
|
|
batch_size: this.importForm.batch_size
|
|
});
|
|
}
|
|
|
|
this.successMessage = `Import started for ${languages.length} language(s)`;
|
|
await this.loadJobs();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to start import:', error);
|
|
this.error = error.message || 'Failed to start import';
|
|
} finally {
|
|
this.importing = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Import from custom URL
|
|
*/
|
|
async startImportFromUrl() {
|
|
if (!this.selectedVendor || !this.importForm.csv_url) return;
|
|
|
|
this.importing = true;
|
|
this.error = '';
|
|
this.successMessage = '';
|
|
this.showImportModal = false;
|
|
|
|
try {
|
|
await apiClient.post('/admin/marketplace-import-jobs', {
|
|
vendor_id: this.selectedVendor.id,
|
|
source_url: this.importForm.csv_url,
|
|
marketplace: 'Letzshop',
|
|
language: this.importForm.language,
|
|
batch_size: this.importForm.batch_size
|
|
});
|
|
|
|
this.successMessage = 'Import job started successfully';
|
|
this.importForm.csv_url = '';
|
|
await this.loadJobs();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to start import:', error);
|
|
this.error = error.message || 'Failed to start import';
|
|
} finally {
|
|
this.importing = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Legacy method for backwards compatibility
|
|
*/
|
|
async startImport() {
|
|
return this.startImportFromUrl();
|
|
},
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// PRODUCTS TAB - EXPORT
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Export products for all languages to Letzshop pickup folder
|
|
*/
|
|
async exportAllLanguages() {
|
|
if (!this.selectedVendor) return;
|
|
|
|
this.exporting = true;
|
|
this.error = '';
|
|
this.successMessage = '';
|
|
|
|
try {
|
|
const response = await apiClient.post(`/admin/vendors/${this.selectedVendor.id}/export/letzshop`, {
|
|
include_inactive: this.exportIncludeInactive
|
|
});
|
|
|
|
this.successMessage = response.message || 'Export completed. CSV files are ready for Letzshop pickup.';
|
|
marketplaceLetzshopLog.info('Export completed:', response);
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to export:', error);
|
|
this.error = error.message || 'Failed to export products';
|
|
} finally {
|
|
this.exporting = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Legacy download export method
|
|
*/
|
|
async downloadExport() {
|
|
return this.exportAllLanguages();
|
|
},
|
|
|
|
/**
|
|
* Format price for display
|
|
*/
|
|
formatPrice(price, currency = 'EUR') {
|
|
if (price == null) return '-';
|
|
return new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: currency
|
|
}).format(price);
|
|
},
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// ORDERS TAB
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Load orders for selected vendor (or all vendors if none selected)
|
|
*/
|
|
async loadOrders() {
|
|
this.loadingOrders = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
skip: ((this.pagination.page - 1) * this.pagination.per_page).toString(),
|
|
limit: this.pagination.per_page.toString()
|
|
});
|
|
|
|
if (this.ordersFilter) {
|
|
params.append('status', this.ordersFilter);
|
|
}
|
|
|
|
if (this.ordersHasDeclinedItems) {
|
|
params.append('has_declined_items', 'true');
|
|
}
|
|
|
|
if (this.ordersSearch) {
|
|
params.append('search', this.ordersSearch);
|
|
}
|
|
|
|
// Use cross-vendor endpoint (with optional vendor_id filter)
|
|
let url = '/admin/letzshop/orders';
|
|
if (this.selectedVendor) {
|
|
params.append('vendor_id', this.selectedVendor.id.toString());
|
|
}
|
|
|
|
const response = await apiClient.get(`${url}?${params}`);
|
|
this.orders = response.orders || [];
|
|
this.pagination.total = response.total || 0;
|
|
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
|
|
|
|
// Use server-side stats (counts all orders, not just visible page)
|
|
if (response.stats) {
|
|
this.orderStats = response.stats;
|
|
} else {
|
|
// Fallback to client-side calculation for backwards compatibility
|
|
this.updateOrderStats();
|
|
}
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to load orders:', error);
|
|
this.error = error.message || 'Failed to load orders';
|
|
} finally {
|
|
this.loadingOrders = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update order stats based on current orders (fallback method)
|
|
*
|
|
* Note: Server now returns stats with all orders counted.
|
|
* This method is kept as a fallback for backwards compatibility.
|
|
*/
|
|
updateOrderStats() {
|
|
// Reset stats
|
|
this.orderStats = { pending: 0, processing: 0, shipped: 0, delivered: 0, cancelled: 0, total: 0, has_declined_items: 0 };
|
|
|
|
// Count from orders list (only visible page - not accurate for totals)
|
|
for (const order of this.orders) {
|
|
if (this.orderStats.hasOwnProperty(order.status)) {
|
|
this.orderStats[order.status]++;
|
|
}
|
|
this.orderStats.total++;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Import orders from Letzshop
|
|
*/
|
|
async importOrders() {
|
|
if (!this.selectedVendor || !this.letzshopStatus.is_configured) return;
|
|
|
|
this.importingOrders = true;
|
|
this.error = '';
|
|
this.successMessage = '';
|
|
|
|
try {
|
|
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/sync`);
|
|
this.successMessage = 'Orders imported successfully';
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to import orders:', error);
|
|
this.error = error.message || 'Failed to import orders';
|
|
} finally {
|
|
this.importingOrders = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Import historical orders from Letzshop (confirmed and declined orders)
|
|
* Uses background job with polling for progress tracking
|
|
*/
|
|
async importHistoricalOrders() {
|
|
if (!this.selectedVendor || !this.letzshopStatus.is_configured) return;
|
|
|
|
this.importingHistorical = true;
|
|
this.error = '';
|
|
this.successMessage = '';
|
|
this.historicalImportResult = null;
|
|
this.historicalImportProgress = {
|
|
status: 'starting',
|
|
message: 'Starting historical import...',
|
|
current_phase: null,
|
|
current_page: 0,
|
|
shipments_fetched: 0,
|
|
orders_processed: 0,
|
|
};
|
|
|
|
try {
|
|
// Start the import job
|
|
const response = await apiClient.post(
|
|
`/admin/letzshop/vendors/${this.selectedVendor.id}/import-history`
|
|
);
|
|
|
|
this.historicalImportJobId = response.job_id;
|
|
marketplaceLetzshopLog.info('Historical import job started:', response);
|
|
|
|
// Start polling for progress
|
|
this.startHistoricalImportPolling();
|
|
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to start historical import:', error);
|
|
this.error = error.message || 'Failed to start historical import';
|
|
this.importingHistorical = false;
|
|
this.historicalImportProgress = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Start polling for historical import progress
|
|
*/
|
|
startHistoricalImportPolling() {
|
|
// Poll every 2 seconds
|
|
this.historicalImportPollInterval = setInterval(async () => {
|
|
await this.pollHistoricalImportStatus();
|
|
}, 2000);
|
|
},
|
|
|
|
/**
|
|
* Poll historical import status
|
|
*/
|
|
async pollHistoricalImportStatus() {
|
|
if (!this.historicalImportJobId || !this.selectedVendor) {
|
|
this.stopHistoricalImportPolling();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const status = await apiClient.get(
|
|
`/admin/letzshop/vendors/${this.selectedVendor.id}/import-history/${this.historicalImportJobId}/status`
|
|
);
|
|
|
|
// Update progress display
|
|
this.historicalImportProgress = {
|
|
status: status.status,
|
|
message: this.formatProgressMessage(status),
|
|
current_phase: status.current_phase,
|
|
current_page: status.current_page,
|
|
total_pages: status.total_pages,
|
|
shipments_fetched: status.shipments_fetched,
|
|
orders_processed: status.orders_processed,
|
|
};
|
|
|
|
// Check if complete or failed
|
|
if (status.status === 'completed' || status.status === 'failed') {
|
|
this.stopHistoricalImportPolling();
|
|
this.importingHistorical = false;
|
|
|
|
if (status.status === 'completed') {
|
|
// Combine stats from both phases
|
|
const confirmed = status.confirmed_stats || {};
|
|
const pending = status.declined_stats || {}; // Actually unconfirmed/pending
|
|
|
|
this.historicalImportResult = {
|
|
imported: (confirmed.imported || 0) + (pending.imported || 0),
|
|
updated: (confirmed.updated || 0) + (pending.updated || 0),
|
|
skipped: (confirmed.skipped || 0) + (pending.skipped || 0),
|
|
products_matched: (confirmed.products_matched || 0) + (pending.products_matched || 0),
|
|
products_not_found: (confirmed.products_not_found || 0) + (pending.products_not_found || 0),
|
|
};
|
|
const stats = this.historicalImportResult;
|
|
|
|
// Build a meaningful summary message
|
|
const parts = [];
|
|
if (stats.imported > 0) parts.push(`${stats.imported} imported`);
|
|
if (stats.updated > 0) parts.push(`${stats.updated} updated`);
|
|
if (stats.skipped > 0) parts.push(`${stats.skipped} already synced`);
|
|
|
|
this.successMessage = parts.length > 0
|
|
? `Historical import complete: ${parts.join(', ')}`
|
|
: 'Historical import complete: no orders found';
|
|
marketplaceLetzshopLog.info('Historical import completed:', status);
|
|
|
|
// Reload orders to show new data
|
|
await this.loadOrders();
|
|
} else {
|
|
this.error = status.error_message || 'Historical import failed';
|
|
marketplaceLetzshopLog.error('Historical import failed:', status);
|
|
}
|
|
|
|
this.historicalImportProgress = null;
|
|
this.historicalImportJobId = null;
|
|
}
|
|
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to poll import status:', error);
|
|
// Don't stop polling on transient errors
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Stop polling for historical import progress
|
|
*/
|
|
stopHistoricalImportPolling() {
|
|
if (this.historicalImportPollInterval) {
|
|
clearInterval(this.historicalImportPollInterval);
|
|
this.historicalImportPollInterval = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Format progress message for display
|
|
*/
|
|
formatProgressMessage(status) {
|
|
// Map phase to display name
|
|
const phaseNames = {
|
|
'confirmed': 'confirmed',
|
|
'unconfirmed': 'pending',
|
|
'declined': 'declined', // Legacy support
|
|
};
|
|
const phase = phaseNames[status.current_phase] || status.current_phase || 'orders';
|
|
|
|
if (status.status === 'fetching') {
|
|
if (status.total_pages) {
|
|
return `Fetching ${phase} orders: page ${status.current_page} of ${status.total_pages} (${status.shipments_fetched} fetched)`;
|
|
}
|
|
return `Fetching ${phase} orders: page ${status.current_page}... (${status.shipments_fetched} fetched)`;
|
|
}
|
|
if (status.status === 'processing') {
|
|
return `Processing ${phase} orders: ${status.orders_processed} processed...`;
|
|
}
|
|
if (status.status === 'pending') {
|
|
return 'Starting historical import...';
|
|
}
|
|
return status.status.charAt(0).toUpperCase() + status.status.slice(1);
|
|
},
|
|
|
|
/**
|
|
* Confirm an order
|
|
*/
|
|
async confirmOrder(order) {
|
|
if (!this.selectedVendor) return;
|
|
|
|
try {
|
|
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/confirm`);
|
|
this.successMessage = 'Order confirmed';
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to confirm order:', error);
|
|
this.error = error.message || 'Failed to confirm order';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Decline an order (all items)
|
|
*/
|
|
async declineOrder(order) {
|
|
if (!this.selectedVendor) return;
|
|
|
|
if (!confirm('Are you sure you want to decline this order? All items will be marked as unavailable.')) return;
|
|
|
|
try {
|
|
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/reject`);
|
|
this.successMessage = 'Order declined';
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to decline order:', error);
|
|
this.error = error.message || 'Failed to decline order';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Open tracking modal
|
|
*/
|
|
openTrackingModal(order) {
|
|
this.selectedOrder = order;
|
|
this.trackingForm = {
|
|
tracking_number: order.tracking_number || '',
|
|
tracking_provider: order.tracking_provider || ''
|
|
};
|
|
this.showTrackingModal = true;
|
|
},
|
|
|
|
/**
|
|
* Submit tracking information
|
|
*/
|
|
async submitTracking() {
|
|
if (!this.selectedVendor || !this.selectedOrder) return;
|
|
|
|
this.submittingTracking = true;
|
|
|
|
try {
|
|
await apiClient.post(
|
|
`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${this.selectedOrder.id}/tracking`,
|
|
this.trackingForm
|
|
);
|
|
this.successMessage = 'Tracking information saved';
|
|
this.showTrackingModal = false;
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to save tracking:', error);
|
|
this.error = error.message || 'Failed to save tracking';
|
|
} finally {
|
|
this.submittingTracking = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* View order details
|
|
*/
|
|
viewOrderDetails(order) {
|
|
this.selectedOrder = order;
|
|
this.showOrderModal = true;
|
|
},
|
|
|
|
/**
|
|
* Confirm a single order item
|
|
*/
|
|
async confirmInventoryUnit(order, item, index) {
|
|
if (!this.selectedVendor) return;
|
|
|
|
// Use external_item_id (Letzshop inventory unit ID)
|
|
const itemId = item.external_item_id;
|
|
if (!itemId) {
|
|
this.error = 'Item has no external ID';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await apiClient.post(
|
|
`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/items/${itemId}/confirm`
|
|
);
|
|
// Update local state
|
|
this.selectedOrder.items[index].item_state = 'confirmed_available';
|
|
this.successMessage = 'Item confirmed';
|
|
// Reload orders to get updated status
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to confirm item:', error);
|
|
this.error = error.message || 'Failed to confirm item';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Decline a single order item
|
|
*/
|
|
async declineInventoryUnit(order, item, index) {
|
|
if (!this.selectedVendor) return;
|
|
|
|
// Use external_item_id (Letzshop inventory unit ID)
|
|
const itemId = item.external_item_id;
|
|
if (!itemId) {
|
|
this.error = 'Item has no external ID';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await apiClient.post(
|
|
`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/items/${itemId}/decline`
|
|
);
|
|
// Update local state
|
|
this.selectedOrder.items[index].item_state = 'confirmed_unavailable';
|
|
this.successMessage = 'Item declined';
|
|
// Reload orders to get updated status
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to decline item:', error);
|
|
this.error = error.message || 'Failed to decline item';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Confirm all items in an order
|
|
*/
|
|
async confirmAllItems(order) {
|
|
if (!this.selectedVendor) return;
|
|
|
|
if (!confirm('Are you sure you want to confirm all items in this order?')) return;
|
|
|
|
try {
|
|
await apiClient.post(
|
|
`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/confirm`
|
|
);
|
|
this.successMessage = 'All items confirmed';
|
|
this.showOrderModal = false;
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to confirm all items:', error);
|
|
this.error = error.message || 'Failed to confirm all items';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Decline all items in an order
|
|
*/
|
|
async declineAllItems(order) {
|
|
if (!this.selectedVendor) return;
|
|
|
|
if (!confirm('Are you sure you want to decline all items in this order?')) return;
|
|
|
|
try {
|
|
await apiClient.post(
|
|
`/admin/letzshop/vendors/${this.selectedVendor.id}/orders/${order.id}/reject`
|
|
);
|
|
this.successMessage = 'All items declined';
|
|
this.showOrderModal = false;
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to decline all items:', error);
|
|
this.error = error.message || 'Failed to decline all items';
|
|
}
|
|
},
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// SETTINGS TAB
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Save Letzshop credentials
|
|
*/
|
|
async saveCredentials() {
|
|
if (!this.selectedVendor) return;
|
|
|
|
this.savingCredentials = true;
|
|
this.error = '';
|
|
this.successMessage = '';
|
|
|
|
try {
|
|
const payload = {
|
|
auto_sync_enabled: this.settingsForm.auto_sync_enabled,
|
|
sync_interval_minutes: parseInt(this.settingsForm.sync_interval_minutes),
|
|
test_mode_enabled: this.settingsForm.test_mode_enabled
|
|
};
|
|
|
|
// Only include API key if it was provided (not just placeholder)
|
|
if (this.settingsForm.api_key && this.settingsForm.api_key.length > 0) {
|
|
payload.api_key = this.settingsForm.api_key;
|
|
}
|
|
|
|
if (this.credentials) {
|
|
// Update existing
|
|
await apiClient.patch(`/admin/letzshop/vendors/${this.selectedVendor.id}/credentials`, payload);
|
|
} else {
|
|
// Create new (API key required)
|
|
if (!payload.api_key) {
|
|
this.error = 'API key is required for initial setup';
|
|
this.savingCredentials = false;
|
|
return;
|
|
}
|
|
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/credentials`, payload);
|
|
}
|
|
|
|
this.successMessage = 'Credentials saved successfully';
|
|
this.settingsForm.api_key = ''; // Clear the input
|
|
await this.loadLetzshopStatus();
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to save credentials:', error);
|
|
this.error = error.message || 'Failed to save credentials';
|
|
} finally {
|
|
this.savingCredentials = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Test Letzshop connection
|
|
*/
|
|
async testConnection() {
|
|
if (!this.selectedVendor || !this.letzshopStatus.is_configured) return;
|
|
|
|
this.testingConnection = true;
|
|
this.error = '';
|
|
this.successMessage = '';
|
|
|
|
try {
|
|
await apiClient.post(`/admin/letzshop/vendors/${this.selectedVendor.id}/test`);
|
|
this.successMessage = 'Connection test successful!';
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Connection test failed:', error);
|
|
this.error = error.message || 'Connection test failed';
|
|
} finally {
|
|
this.testingConnection = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Delete Letzshop credentials
|
|
*/
|
|
async deleteCredentials() {
|
|
if (!this.selectedVendor) return;
|
|
|
|
if (!confirm('Are you sure you want to remove the Letzshop configuration? This will disable all Letzshop features for this vendor.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await apiClient.delete(`/admin/letzshop/vendors/${this.selectedVendor.id}/credentials`);
|
|
this.successMessage = 'Credentials removed';
|
|
this.credentials = null;
|
|
this.letzshopStatus = { is_configured: false };
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to delete credentials:', error);
|
|
this.error = error.message || 'Failed to remove credentials';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Save CSV URLs to vendor
|
|
*/
|
|
async saveCsvUrls() {
|
|
if (!this.selectedVendor) return;
|
|
|
|
this.savingCsvUrls = true;
|
|
this.error = '';
|
|
this.successMessage = '';
|
|
|
|
try {
|
|
await apiClient.patch(`/admin/vendors/${this.selectedVendor.id}`, {
|
|
letzshop_csv_url_fr: this.settingsForm.letzshop_csv_url_fr || null,
|
|
letzshop_csv_url_en: this.settingsForm.letzshop_csv_url_en || null,
|
|
letzshop_csv_url_de: this.settingsForm.letzshop_csv_url_de || null
|
|
});
|
|
|
|
// Update local vendor object
|
|
this.selectedVendor.letzshop_csv_url_fr = this.settingsForm.letzshop_csv_url_fr;
|
|
this.selectedVendor.letzshop_csv_url_en = this.settingsForm.letzshop_csv_url_en;
|
|
this.selectedVendor.letzshop_csv_url_de = this.settingsForm.letzshop_csv_url_de;
|
|
|
|
this.successMessage = 'CSV URLs saved successfully';
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to save CSV URLs:', error);
|
|
this.error = error.message || 'Failed to save CSV URLs';
|
|
} finally {
|
|
this.savingCsvUrls = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Save carrier settings
|
|
*/
|
|
async saveCarrierSettings() {
|
|
if (!this.selectedVendor || !this.credentials) return;
|
|
|
|
this.savingCarrierSettings = true;
|
|
this.error = '';
|
|
this.successMessage = '';
|
|
|
|
try {
|
|
await apiClient.patch(`/admin/letzshop/vendors/${this.selectedVendor.id}/credentials`, {
|
|
default_carrier: this.settingsForm.default_carrier || null,
|
|
carrier_greco_label_url: this.settingsForm.carrier_greco_label_url || null,
|
|
carrier_colissimo_label_url: this.settingsForm.carrier_colissimo_label_url || null,
|
|
carrier_xpresslogistics_label_url: this.settingsForm.carrier_xpresslogistics_label_url || null
|
|
});
|
|
|
|
this.successMessage = 'Carrier settings saved successfully';
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to save carrier settings:', error);
|
|
this.error = error.message || 'Failed to save carrier settings';
|
|
} finally {
|
|
this.savingCarrierSettings = false;
|
|
}
|
|
},
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// EXCEPTIONS
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Load exceptions for selected vendor (or all vendors if none selected)
|
|
*/
|
|
async loadExceptions() {
|
|
this.loadingExceptions = true;
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
skip: ((this.pagination.page - 1) * this.pagination.per_page).toString(),
|
|
limit: this.pagination.per_page.toString()
|
|
});
|
|
|
|
if (this.exceptionsFilter) {
|
|
params.append('status', this.exceptionsFilter);
|
|
}
|
|
|
|
if (this.exceptionsSearch) {
|
|
params.append('search', this.exceptionsSearch);
|
|
}
|
|
|
|
// Add vendor filter if a vendor is selected
|
|
if (this.selectedVendor) {
|
|
params.append('vendor_id', this.selectedVendor.id.toString());
|
|
}
|
|
|
|
const response = await apiClient.get(`/admin/order-exceptions?${params}`);
|
|
this.exceptions = response.exceptions || [];
|
|
this.pagination.total = response.total || 0;
|
|
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to load exceptions:', error);
|
|
this.error = error.message || 'Failed to load exceptions';
|
|
} finally {
|
|
this.loadingExceptions = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load exception statistics for selected vendor (or all vendors if none selected)
|
|
*/
|
|
async loadExceptionStats() {
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (this.selectedVendor) {
|
|
params.append('vendor_id', this.selectedVendor.id.toString());
|
|
}
|
|
|
|
const response = await apiClient.get(`/admin/order-exceptions/stats?${params}`);
|
|
this.exceptionStats = response;
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to load exception stats:', error);
|
|
this.exceptionStats = { pending: 0, resolved: 0, ignored: 0, total: 0, orders_with_exceptions: 0 };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Open the resolve modal for an exception
|
|
*/
|
|
openResolveModal(exception) {
|
|
this.selectedExceptionForResolve = exception;
|
|
this.resolveForm = { product_id: null, product_name: '', notes: '', bulk_resolve: false };
|
|
this.productSearchQuery = '';
|
|
this.productSearchResults = [];
|
|
this.showResolveModal = true;
|
|
},
|
|
|
|
/**
|
|
* Search for products to assign to exception
|
|
*/
|
|
async searchProducts() {
|
|
if (!this.productSearchQuery || this.productSearchQuery.length < 2) {
|
|
this.productSearchResults = [];
|
|
return;
|
|
}
|
|
|
|
this.searchingProducts = true;
|
|
|
|
try {
|
|
const response = await apiClient.get(`/admin/products?vendor_id=${this.selectedVendor.id}&search=${encodeURIComponent(this.productSearchQuery)}&limit=10`);
|
|
this.productSearchResults = response.products || [];
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to search products:', error);
|
|
this.productSearchResults = [];
|
|
} finally {
|
|
this.searchingProducts = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Select a product for resolving exception
|
|
*/
|
|
selectProductForResolve(product) {
|
|
this.resolveForm.product_id = product.id;
|
|
this.resolveForm.product_name = product.name || product.title;
|
|
this.productSearchResults = [];
|
|
this.productSearchQuery = '';
|
|
},
|
|
|
|
/**
|
|
* Submit exception resolution
|
|
*/
|
|
async submitResolveException() {
|
|
if (!this.selectedExceptionForResolve || !this.resolveForm.product_id) return;
|
|
|
|
this.submittingResolve = true;
|
|
|
|
try {
|
|
if (this.resolveForm.bulk_resolve && this.selectedExceptionForResolve.original_gtin) {
|
|
// Bulk resolve by GTIN
|
|
const response = await apiClient.post(`/admin/order-exceptions/bulk-resolve?vendor_id=${this.selectedVendor.id}`, {
|
|
gtin: this.selectedExceptionForResolve.original_gtin,
|
|
product_id: this.resolveForm.product_id,
|
|
notes: this.resolveForm.notes
|
|
});
|
|
this.successMessage = `Resolved ${response.resolved_count} exception(s) for GTIN ${response.gtin}`;
|
|
} else {
|
|
// Single resolve
|
|
await apiClient.post(`/admin/order-exceptions/${this.selectedExceptionForResolve.id}/resolve`, {
|
|
product_id: this.resolveForm.product_id,
|
|
notes: this.resolveForm.notes
|
|
});
|
|
this.successMessage = 'Exception resolved successfully';
|
|
}
|
|
|
|
this.showResolveModal = false;
|
|
await Promise.all([
|
|
this.loadExceptions(),
|
|
this.loadExceptionStats()
|
|
]);
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to resolve exception:', error);
|
|
this.error = error.message || 'Failed to resolve exception';
|
|
} finally {
|
|
this.submittingResolve = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ignore an exception
|
|
*/
|
|
async ignoreException(exception) {
|
|
if (!confirm('Are you sure you want to ignore this exception? The order will still be blocked from confirmation.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await apiClient.post(`/admin/order-exceptions/${exception.id}/ignore`, {
|
|
notes: 'Ignored via admin interface'
|
|
});
|
|
this.successMessage = 'Exception marked as ignored';
|
|
await Promise.all([
|
|
this.loadExceptions(),
|
|
this.loadExceptionStats()
|
|
]);
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to ignore exception:', error);
|
|
this.error = error.message || 'Failed to ignore exception';
|
|
}
|
|
},
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// JOBS TABLE
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Load jobs for selected vendor or all vendors
|
|
*/
|
|
async loadJobs() {
|
|
this.loadingJobs = true;
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
skip: ((this.pagination.page - 1) * this.pagination.per_page).toString(),
|
|
limit: this.pagination.per_page.toString()
|
|
});
|
|
|
|
if (this.jobsFilter.type) {
|
|
params.append('job_type', this.jobsFilter.type);
|
|
}
|
|
if (this.jobsFilter.status) {
|
|
params.append('status', this.jobsFilter.status);
|
|
}
|
|
|
|
// Use vendor-specific or global endpoint based on selection
|
|
const endpoint = this.selectedVendor
|
|
? `/admin/letzshop/vendors/${this.selectedVendor.id}/jobs?${params}`
|
|
: `/admin/letzshop/jobs?${params}`;
|
|
|
|
const response = await apiClient.get(endpoint);
|
|
this.jobs = response.jobs || [];
|
|
this.pagination.total = response.total || 0;
|
|
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to load jobs:', error);
|
|
// Don't show error for jobs - not critical
|
|
} finally {
|
|
this.loadingJobs = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* View job details in modal
|
|
*/
|
|
viewJobDetails(job) {
|
|
marketplaceLetzshopLog.info('View job details:', job);
|
|
this.selectedJobDetails = job;
|
|
this.showJobDetailsModal = true;
|
|
},
|
|
|
|
/**
|
|
* View job errors
|
|
*/
|
|
async viewJobErrors(job) {
|
|
if (job.type !== 'import' && job.type !== 'historical_import') return;
|
|
|
|
try {
|
|
const endpoint = job.type === 'import'
|
|
? `/admin/marketplace-import-jobs/${job.id}/errors`
|
|
: `/admin/letzshop/historical-imports/${job.id}`;
|
|
|
|
const response = await apiClient.get(endpoint);
|
|
|
|
if (job.type === 'import') {
|
|
const errors = response.errors || [];
|
|
if (errors.length === 0) {
|
|
Utils.showToast('No error details available', 'info');
|
|
return;
|
|
}
|
|
// Store errors and show in job details modal
|
|
this.selectedJobDetails = { ...job, errors: errors.slice(0, 20) };
|
|
this.showJobDetailsModal = true;
|
|
} else {
|
|
// Historical import - show job details
|
|
this.selectedJobDetails = { ...job, ...response };
|
|
this.showJobDetailsModal = true;
|
|
}
|
|
} catch (error) {
|
|
marketplaceLetzshopLog.error('Failed to load job errors:', error);
|
|
Utils.showToast('Failed to load error details', 'error');
|
|
}
|
|
},
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// UTILITIES
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Format date for display
|
|
*/
|
|
formatDate(dateString) {
|
|
if (!dateString) return 'N/A';
|
|
try {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('en-GB', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
} catch {
|
|
return dateString;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Format duration between two dates
|
|
*/
|
|
formatDuration(startDate, endDate) {
|
|
if (!startDate) return '-';
|
|
if (!endDate) return 'In progress...';
|
|
|
|
try {
|
|
const start = new Date(startDate);
|
|
const end = new Date(endDate);
|
|
const diffMs = end - start;
|
|
|
|
if (diffMs < 1000) return '<1s';
|
|
if (diffMs < 60000) return `${Math.round(diffMs / 1000)}s`;
|
|
if (diffMs < 3600000) return `${Math.round(diffMs / 60000)}m`;
|
|
return `${Math.round(diffMs / 3600000)}h`;
|
|
} catch {
|
|
return '-';
|
|
}
|
|
}
|
|
};
|
|
}
|