From 8e8d1d1ac04b417c30442113054b4f7f1d4ad186 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Fri, 19 Dec 2025 21:18:48 +0100 Subject: [PATCH] wip: update frontend templates for Letzshop order management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Letzshop order detail page template - Update orders list template - Update Letzshop orders tab with improved UI - Add JavaScript for order confirmation flow Note: Frontend needs alignment with new unified order schema. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../admin/letzshop-order-detail.html | 296 ++++++++++++++++++ app/templates/admin/marketplace-letzshop.html | 83 ++++- app/templates/admin/orders.html | 83 ++++- .../admin/partials/letzshop-orders-tab.html | 65 +++- static/admin/js/marketplace-letzshop.js | 187 +++++++++-- static/admin/js/orders.js | 85 +++++ 6 files changed, 739 insertions(+), 60 deletions(-) create mode 100644 app/templates/admin/letzshop-order-detail.html diff --git a/app/templates/admin/letzshop-order-detail.html b/app/templates/admin/letzshop-order-detail.html new file mode 100644 index 00000000..f12b5ad2 --- /dev/null +++ b/app/templates/admin/letzshop-order-detail.html @@ -0,0 +1,296 @@ +{# app/templates/admin/letzshop-order-detail.html #} +{% extends "admin/base.html" %} +{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %} +{% from 'shared/macros/headers.html' import page_header_flex %} + +{% block title %}Letzshop Order Details{% endblock %} +{% block alpine_data %}letzshopOrderDetail(){% endblock %} + +{% block content %} +
+
+ +
+
+ + + +
+

+ Order +

+

+ Letzshop Order Details +

+
+
+
+ +
+
+ + +
+
+
+ + + {{ error_state('Failed to load order', 'error') }} + + +
+ +
+

+ + Order Information +

+
+
+ Order Number + +
+
+ Order Date + +
+
+ Shipment ID + +
+
+ Total + +
+
+ Confirmed At + +
+
+ Declined At + +
+
+
+ + +
+

+ + Customer Information +

+
+
+ Name + +
+
+ Email + +
+
+ Language + +
+
+
+ + +
+

+ + Shipping Address +

+
+

+

+

+

+

+

+ Phone: + +

+
+
+ + +
+

+ + Tracking Information +

+
+
+ Carrier + +
+
+ Tracking Number + +
+
+ Set At + +
+
+
+ No tracking information available +
+
+
+ + +
+

+ + Order Items + + ( items) + +

+ +
+ + + + + + + + + + + + +
ProductEAN/SKUPriceStatus
+
+
+ + +
+ +
+
+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/admin/marketplace-letzshop.html b/app/templates/admin/marketplace-letzshop.html index d695fb4f..80ac1493 100644 --- a/app/templates/admin/marketplace-letzshop.html +++ b/app/templates/admin/marketplace-letzshop.html @@ -252,18 +252,33 @@ @click.stop >
-

Order Details

+
+

Order Details

+ + + Full View + +
+
Order Number:
+
+ Order Date: + +
Status:
-
- Customer: - -
Total:
-
- Tracking: - -
-
- Created: - +
+ + +
+

+ + Customer & Shipping +

+
+
+

+ Name: + +

+

+ Email: + +

+

+ Language: + +

+
+
+

+ Ship to: + +

+
-
-

+ +
+

+ + Tracking +

+
+

+ Carrier: + +

+

+ Tracking #: + +

+
+
+ + +
+

+ Items - (Each item must be confirmed or declined individually) + ( items)

diff --git a/app/templates/admin/orders.html b/app/templates/admin/orders.html index 7b951588..2a1f0445 100644 --- a/app/templates/admin/orders.html +++ b/app/templates/admin/orders.html @@ -1,7 +1,7 @@ {# app/templates/admin/orders.html #} {% extends "admin/base.html" %} {% from 'shared/macros/pagination.html' import pagination %} -{% from 'shared/macros/headers.html' import page_header %} +{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %} {% from 'shared/macros/alerts.html' import loading_state, error_state %} {% from 'shared/macros/tables.html' import table_wrapper %} {% from 'shared/macros/modals.html' import modal_simple %} @@ -11,8 +11,79 @@ {% block alpine_data %}adminOrders(){% endblock %} +{% block extra_head %} + + + +{% endblock %} + {% block content %} -{{ page_header('Orders', subtitle='Manage orders across all vendors') }} + +{% call page_header_flex(title='Orders', subtitle='Manage orders across all vendors') %} +
+ +
+ +
+ {{ refresh_button(loading_var='loading', onclick='refresh()', variant='secondary') }} +
+{% endcall %} + + +
+
+
+
+ +
+
+ + +
+
+ +
+
{{ loading_state('Loading orders...') }} @@ -102,14 +173,6 @@
- - {{ vendor_selector( - ref_name='vendorSelect', - id='orders-vendor-select', - placeholder='Filter by vendor...', - width='w-64' - ) }} - + +
+ + + + +
@@ -199,7 +254,7 @@ > - +
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 */