diff --git a/app/templates/admin/marketplace-letzshop.html b/app/templates/admin/marketplace-letzshop.html
index 55366b59..8e6b44d1 100644
--- a/app/templates/admin/marketplace-letzshop.html
+++ b/app/templates/admin/marketplace-letzshop.html
@@ -119,6 +119,7 @@
{% call tabs_nav(tab_var='activeTab') %}
{{ tab_button('products', 'Products', tab_var='activeTab', icon='cube') }}
{{ tab_button('orders', 'Orders', tab_var='activeTab', icon='shopping-cart', count_var='orderStats.pending') }}
+ {{ tab_button('exceptions', 'Exceptions', tab_var='activeTab', icon='exclamation-circle', count_var='exceptionStats.pending') }}
{{ tab_button('settings', 'Settings', tab_var='activeTab', icon='cog') }}
{% endcall %}
@@ -137,6 +138,11 @@
{% include 'admin/partials/letzshop-settings-tab.html' %}
{{ endtab_panel() }}
+
+ {{ tab_panel('exceptions', tab_var='activeTab') }}
+ {% include 'admin/partials/letzshop-exceptions-tab.html' %}
+ {{ endtab_panel() }}
+
{% include 'admin/partials/letzshop-jobs-table.html' %}
@@ -426,6 +432,146 @@
+
+
+
+
+
+ Resolve Exception
+
+
+
+
+
+
+
+
+ GTIN:
+
+
+
+ SKU:
+
+
+ Order:
+
+
+
+
+
+
+
+
{% endblock %}
{% block extra_scripts %}
diff --git a/app/templates/admin/partials/letzshop-exceptions-tab.html b/app/templates/admin/partials/letzshop-exceptions-tab.html
new file mode 100644
index 00000000..b6ce83bd
--- /dev/null
+++ b/app/templates/admin/partials/letzshop-exceptions-tab.html
@@ -0,0 +1,234 @@
+{# app/templates/admin/partials/letzshop-exceptions-tab.html #}
+{# Exceptions tab for admin Letzshop management - Order Item Exception Resolution #}
+
+
+
+
+
Product Exceptions
+
Resolve unmatched products from order imports
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Product Info |
+ GTIN |
+ Order |
+ Status |
+ Created |
+ Actions |
+
+
+
+
+
+ |
+
+ Loading exceptions...
+ |
+
+
+
+
+ |
+
+ No exceptions found
+ All order items are properly matched to products
+ |
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+ Resolved
+
+
+
+ |
+
+
+
+
+
+
+
+
+ Showing - of
+
+
+
+
+
+
+
diff --git a/static/admin/js/marketplace-letzshop.js b/static/admin/js/marketplace-letzshop.js
index 6bd624c1..290c07c3 100644
--- a/static/admin/js/marketplace-letzshop.js
+++ b/static/admin/js/marketplace-letzshop.js
@@ -94,6 +94,16 @@ function adminMarketplaceLetzshop() {
ordersHasDeclinedItems: false,
orderStats: { pending: 0, processing: 0, shipped: 0, delivered: 0, cancelled: 0, total: 0, has_declined_items: 0 },
+ // Exceptions
+ exceptions: [],
+ totalExceptions: 0,
+ exceptionsPage: 1,
+ exceptionsLimit: 20,
+ exceptionsFilter: '',
+ exceptionsSearch: '',
+ exceptionStats: { pending: 0, resolved: 0, ignored: 0, total: 0, orders_with_exceptions: 0 },
+ loadingExceptions: false,
+
// Jobs
jobs: [],
jobsFilter: { type: '', status: '' },
@@ -102,8 +112,15 @@ function adminMarketplaceLetzshop() {
// Modals
showTrackingModal: false,
showOrderModal: false,
+ showResolveModal: false,
selectedOrder: null,
+ selectedExceptionForResolve: null,
trackingForm: { tracking_number: '', tracking_provider: '' },
+ resolveForm: { product_id: null, product_name: '', notes: '', bulk_resolve: false },
+ productSearchQuery: '',
+ productSearchResults: [],
+ searchingProducts: false,
+ submittingResolve: false,
async init() {
marketplaceLetzshopLog.info('init() called');
@@ -208,9 +225,11 @@ function adminMarketplaceLetzshop() {
// Load Letzshop status and credentials
await this.loadLetzshopStatus();
- // Load orders and jobs
+ // Load orders, exceptions, and jobs
await Promise.all([
this.loadOrders(),
+ this.loadExceptions(),
+ this.loadExceptionStats(),
this.loadJobs()
]);
@@ -234,6 +253,10 @@ function adminMarketplaceLetzshop() {
this.ordersFilter = '';
this.ordersSearch = '';
this.ordersHasDeclinedItems = false;
+ this.exceptions = [];
+ this.exceptionsFilter = '';
+ this.exceptionsSearch = '';
+ this.exceptionStats = { pending: 0, resolved: 0, ignored: 0, total: 0, orders_with_exceptions: 0 };
this.jobs = [];
this.settingsForm = {
api_key: '',
@@ -927,6 +950,169 @@ function adminMarketplaceLetzshop() {
}
},
+ // ═══════════════════════════════════════════════════════════════
+ // EXCEPTIONS
+ // ═══════════════════════════════════════════════════════════════
+
+ /**
+ * Load exceptions for selected vendor
+ */
+ async loadExceptions() {
+ if (!this.selectedVendor) {
+ this.exceptions = [];
+ this.totalExceptions = 0;
+ return;
+ }
+
+ this.loadingExceptions = true;
+
+ try {
+ const params = new URLSearchParams({
+ skip: ((this.exceptionsPage - 1) * this.exceptionsLimit).toString(),
+ limit: this.exceptionsLimit.toString()
+ });
+
+ if (this.exceptionsFilter) {
+ params.append('status', this.exceptionsFilter);
+ }
+
+ if (this.exceptionsSearch) {
+ params.append('search', this.exceptionsSearch);
+ }
+
+ const response = await apiClient.get(`/admin/order-exceptions?vendor_id=${this.selectedVendor.id}&${params}`);
+ this.exceptions = response.exceptions || [];
+ this.totalExceptions = response.total || 0;
+ } 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
+ */
+ async loadExceptionStats() {
+ if (!this.selectedVendor) {
+ this.exceptionStats = { pending: 0, resolved: 0, ignored: 0, total: 0, orders_with_exceptions: 0 };
+ return;
+ }
+
+ try {
+ const response = await apiClient.get(`/admin/order-exceptions/stats?vendor_id=${this.selectedVendor.id}`);
+ this.exceptionStats = response;
+ } catch (error) {
+ marketplaceLetzshopLog.error('Failed to load exception stats:', error);
+ }
+ },
+
+ /**
+ * 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
// ═══════════════════════════════════════════════════════════════