feat: add Exceptions tab to Letzshop management page
Adds a new "Exceptions" tab to the Letzshop marketplace page for managing unmatched product exceptions from order imports. Features: - Exception list with search and status filtering - Stats cards showing pending/resolved/ignored counts - Resolve modal with product search - Bulk resolve option for same GTIN - Ignore functionality Files: - New: letzshop-exceptions-tab.html partial template - Updated: marketplace-letzshop.html (tab button, panel, resolve modal) - Updated: marketplace-letzshop.js (exception state, methods) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user