wip: update frontend templates for Letzshop order management

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-12-19 21:18:48 +01:00
parent fceaba703e
commit 8e8d1d1ac0
6 changed files with 739 additions and 60 deletions

View File

@@ -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
*/

View File

@@ -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 `<div class="flex items-center justify-between py-1">
<span>${escape(data.name)}</span>
<span class="text-xs text-gray-400 font-mono">${escape(data.vendor_code || '')}</span>
</div>`;
},
item: (data, escape) => {
return `<div>${escape(data.name)}</div>`;
}
},
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
*/