diff --git a/static/admin/js/marketplace-letzshop.js b/static/admin/js/marketplace-letzshop.js
index 190020b0..66cf476c 100644
--- a/static/admin/js/marketplace-letzshop.js
+++ b/static/admin/js/marketplace-letzshop.js
@@ -35,8 +35,11 @@ function adminMarketplaceLetzshop() {
testingConnection: false,
submittingTracking: false,
- // Historical import result
+ // Historical import state
historicalImportResult: null,
+ historicalImportJobId: null,
+ historicalImportProgress: null,
+ historicalImportPollInterval: null,
// Messages
error: '',
@@ -87,7 +90,9 @@ function adminMarketplaceLetzshop() {
ordersPage: 1,
ordersLimit: 20,
ordersFilter: '',
- orderStats: { pending: 0, confirmed: 0, rejected: 0, shipped: 0 },
+ ordersSearch: '',
+ ordersHasDeclinedItems: false,
+ orderStats: { pending: 0, confirmed: 0, rejected: 0, shipped: 0, has_declined_items: 0 },
// Jobs
jobs: [],
@@ -226,6 +231,9 @@ function adminMarketplaceLetzshop() {
this.letzshopStatus = { is_configured: false };
this.credentials = null;
this.orders = [];
+ this.ordersFilter = '';
+ this.ordersSearch = '';
+ this.ordersHasDeclinedItems = false;
this.jobs = [];
this.settingsForm = {
api_key: '',
@@ -394,6 +402,14 @@ function adminMarketplaceLetzshop() {
params.append('sync_status', this.ordersFilter);
}
+ if (this.ordersHasDeclinedItems) {
+ params.append('has_declined_items', 'true');
+ }
+
+ if (this.ordersSearch) {
+ params.append('search', this.ordersSearch);
+ }
+
const response = await apiClient.get(`/admin/letzshop/vendors/${this.selectedVendor.id}/orders?${params}`);
this.orders = response.orders || [];
this.totalOrders = response.total || 0;
@@ -421,7 +437,7 @@ function adminMarketplaceLetzshop() {
*/
updateOrderStats() {
// Reset stats
- this.orderStats = { pending: 0, confirmed: 0, rejected: 0, shipped: 0 };
+ this.orderStats = { pending: 0, confirmed: 0, rejected: 0, shipped: 0, has_declined_items: 0 };
// Count from orders list (only visible page - not accurate for totals)
for (const order of this.orders) {
@@ -455,6 +471,7 @@ function adminMarketplaceLetzshop() {
/**
* 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;
@@ -463,44 +480,154 @@ function adminMarketplaceLetzshop() {
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 {
- // Import confirmed orders
- const confirmedResponse = await apiClient.post(
- `/admin/letzshop/vendors/${this.selectedVendor.id}/import-history?state=confirmed`
+ // Start the import job
+ const response = await apiClient.post(
+ `/admin/letzshop/vendors/${this.selectedVendor.id}/import-history`
);
- const confirmedStats = confirmedResponse.statistics || confirmedResponse;
- // Import declined (rejected) orders
- const declinedResponse = await apiClient.post(
- `/admin/letzshop/vendors/${this.selectedVendor.id}/import-history?state=declined`
- );
- const declinedStats = declinedResponse.statistics || declinedResponse;
+ this.historicalImportJobId = response.job_id;
+ marketplaceLetzshopLog.info('Historical import job started:', response);
- // Combine stats
- this.historicalImportResult = {
- imported: (confirmedStats.imported || 0) + (declinedStats.imported || 0),
- updated: (confirmedStats.updated || 0) + (declinedStats.updated || 0),
- skipped: (confirmedStats.skipped || 0) + (declinedStats.skipped || 0),
- products_matched: (confirmedStats.products_matched || 0) + (declinedStats.products_matched || 0),
- products_not_found: (confirmedStats.products_not_found || 0) + (declinedStats.products_not_found || 0),
- };
- const stats = this.historicalImportResult;
- this.successMessage = `Historical import complete: ${stats.imported} imported, ${stats.updated} updated`;
+ // Start polling for progress
+ this.startHistoricalImportPolling();
- marketplaceLetzshopLog.info('Historical import result (confirmed):', confirmedResponse);
- marketplaceLetzshopLog.info('Historical import result (declined):', declinedResponse);
-
- // Reload orders to show new data
- await this.loadOrders();
} catch (error) {
- marketplaceLetzshopLog.error('Failed to import historical orders:', error);
- this.error = error.message || 'Failed to import historical orders';
- } finally {
+ 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
*/
diff --git a/static/admin/js/orders.js b/static/admin/js/orders.js
index 8056bc9e..68395109 100644
--- a/static/admin/js/orders.js
+++ b/static/admin/js/orders.js
@@ -49,6 +49,12 @@ function adminOrders() {
// Available vendors for filter dropdown
vendors: [],
+ // Selected vendor (for prominent display)
+ selectedVendor: null,
+
+ // Tom Select instance
+ vendorSelectInstance: null,
+
// Pagination
pagination: {
page: 1,
@@ -128,6 +134,9 @@ function adminOrders() {
}
window._adminOrdersInitialized = true;
+ // Initialize Tom Select for vendor filter
+ this.initVendorSelect();
+
// Load data in parallel
await Promise.all([
this.loadStats(),
@@ -138,6 +147,82 @@ function adminOrders() {
adminOrdersLog.info('Orders initialization complete');
},
+ /**
+ * Initialize Tom Select for vendor autocomplete
+ */
+ initVendorSelect() {
+ const selectEl = this.$refs.vendorSelect;
+ if (!selectEl) {
+ adminOrdersLog.warn('Vendor select element not found');
+ return;
+ }
+
+ // Wait for Tom Select to be available
+ if (typeof TomSelect === 'undefined') {
+ adminOrdersLog.warn('TomSelect not loaded, retrying in 100ms');
+ setTimeout(() => this.initVendorSelect(), 100);
+ return;
+ }
+
+ this.vendorSelectInstance = new TomSelect(selectEl, {
+ valueField: 'id',
+ labelField: 'name',
+ searchField: ['name', 'vendor_code'],
+ placeholder: 'All vendors...',
+ allowEmptyOption: true,
+ load: async (query, callback) => {
+ try {
+ const response = await apiClient.get('/admin/vendors', {
+ search: query,
+ limit: 50
+ });
+ callback(response.vendors || []);
+ } catch (error) {
+ adminOrdersLog.error('Failed to search vendors:', error);
+ callback([]);
+ }
+ },
+ render: {
+ option: (data, escape) => {
+ return `
+ ${escape(data.name)}
+ ${escape(data.vendor_code || '')}
+
`;
+ },
+ item: (data, escape) => {
+ return `
${escape(data.name)}
`;
+ }
+ },
+ onChange: (value) => {
+ if (value) {
+ const vendor = this.vendorSelectInstance.options[value];
+ this.selectedVendor = vendor;
+ this.filters.vendor_id = value;
+ } else {
+ this.selectedVendor = null;
+ this.filters.vendor_id = '';
+ }
+ this.pagination.page = 1;
+ this.loadOrders();
+ }
+ });
+
+ adminOrdersLog.info('Vendor select initialized');
+ },
+
+ /**
+ * Clear vendor filter
+ */
+ clearVendorFilter() {
+ if (this.vendorSelectInstance) {
+ this.vendorSelectInstance.clear();
+ }
+ this.selectedVendor = null;
+ this.filters.vendor_id = '';
+ this.pagination.page = 1;
+ this.loadOrders();
+ },
+
/**
* Load order statistics
*/