Vendor API endpoints use JWT authentication, not URL path parameters. The vendorCode should only be used for page URLs (navigation), not API calls. Fixed API paths in 10 vendor JS files: - analytics.js, customers.js, inventory.js, notifications.js - order-detail.js, orders.js, products.js, profile.js - settings.js, team.js Added architecture rule JS-014 to prevent this pattern from recurring. Added validation check _check_vendor_api_paths to validate_architecture.py. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
480 lines
15 KiB
JavaScript
480 lines
15 KiB
JavaScript
// static/vendor/js/orders.js
|
|
/**
|
|
* Vendor orders management page logic
|
|
* View and manage vendor's orders
|
|
*/
|
|
|
|
const vendorOrdersLog = window.LogConfig.loggers.vendorOrders ||
|
|
window.LogConfig.createLogger('vendorOrders', false);
|
|
|
|
vendorOrdersLog.info('Loading...');
|
|
|
|
function vendorOrders() {
|
|
vendorOrdersLog.info('vendorOrders() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'orders',
|
|
|
|
// Loading states
|
|
loading: true,
|
|
error: '',
|
|
saving: false,
|
|
|
|
// Orders data
|
|
orders: [],
|
|
stats: {
|
|
total: 0,
|
|
pending: 0,
|
|
processing: 0,
|
|
completed: 0,
|
|
cancelled: 0
|
|
},
|
|
|
|
// Order statuses for filter and display
|
|
statuses: [
|
|
{ value: 'pending', label: 'Pending', color: 'yellow' },
|
|
{ value: 'processing', label: 'Processing', color: 'blue' },
|
|
{ value: 'partially_shipped', label: 'Partially Shipped', color: 'orange' },
|
|
{ value: 'shipped', label: 'Shipped', color: 'indigo' },
|
|
{ value: 'delivered', label: 'Delivered', color: 'green' },
|
|
{ value: 'completed', label: 'Completed', color: 'green' },
|
|
{ value: 'cancelled', label: 'Cancelled', color: 'red' },
|
|
{ value: 'refunded', label: 'Refunded', color: 'gray' }
|
|
],
|
|
|
|
// Filters
|
|
filters: {
|
|
search: '',
|
|
status: '',
|
|
date_from: '',
|
|
date_to: ''
|
|
},
|
|
|
|
// Pagination
|
|
pagination: {
|
|
page: 1,
|
|
per_page: 20,
|
|
total: 0,
|
|
pages: 0
|
|
},
|
|
|
|
// Modal states
|
|
showDetailModal: false,
|
|
showStatusModal: false,
|
|
showBulkStatusModal: false,
|
|
selectedOrder: null,
|
|
newStatus: '',
|
|
bulkStatus: '',
|
|
|
|
// Bulk selection
|
|
selectedOrders: [],
|
|
|
|
// Debounce timer
|
|
searchTimeout: null,
|
|
|
|
// Computed: Total pages
|
|
get totalPages() {
|
|
return this.pagination.pages;
|
|
},
|
|
|
|
// Computed: Start index for pagination display
|
|
get startIndex() {
|
|
if (this.pagination.total === 0) return 0;
|
|
return (this.pagination.page - 1) * this.pagination.per_page + 1;
|
|
},
|
|
|
|
// Computed: End index for pagination display
|
|
get endIndex() {
|
|
const end = this.pagination.page * this.pagination.per_page;
|
|
return end > this.pagination.total ? this.pagination.total : end;
|
|
},
|
|
|
|
// Computed: Page numbers for pagination
|
|
get pageNumbers() {
|
|
const pages = [];
|
|
const totalPages = this.totalPages;
|
|
const current = this.pagination.page;
|
|
|
|
if (totalPages <= 7) {
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
pages.push(i);
|
|
}
|
|
} else {
|
|
pages.push(1);
|
|
if (current > 3) pages.push('...');
|
|
const start = Math.max(2, current - 1);
|
|
const end = Math.min(totalPages - 1, current + 1);
|
|
for (let i = start; i <= end; i++) {
|
|
pages.push(i);
|
|
}
|
|
if (current < totalPages - 2) pages.push('...');
|
|
pages.push(totalPages);
|
|
}
|
|
return pages;
|
|
},
|
|
|
|
// Computed: Check if all visible orders are selected
|
|
get allSelected() {
|
|
return this.orders.length > 0 && this.selectedOrders.length === this.orders.length;
|
|
},
|
|
|
|
// Computed: Check if some but not all orders are selected
|
|
get someSelected() {
|
|
return this.selectedOrders.length > 0 && this.selectedOrders.length < this.orders.length;
|
|
},
|
|
|
|
async init() {
|
|
vendorOrdersLog.info('Orders init() called');
|
|
|
|
// Guard against multiple initialization
|
|
if (window._vendorOrdersInitialized) {
|
|
vendorOrdersLog.warn('Already initialized, skipping');
|
|
return;
|
|
}
|
|
window._vendorOrdersInitialized = true;
|
|
|
|
// IMPORTANT: Call parent init first to set vendorCode from URL
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
// Load platform settings for rows per page
|
|
if (window.PlatformSettings) {
|
|
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
|
}
|
|
|
|
try {
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
vendorOrdersLog.error('Init failed:', error);
|
|
this.error = 'Failed to initialize orders page';
|
|
}
|
|
|
|
vendorOrdersLog.info('Orders initialization complete');
|
|
},
|
|
|
|
/**
|
|
* Load orders with filtering and pagination
|
|
*/
|
|
async loadOrders() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
skip: (this.pagination.page - 1) * this.pagination.per_page,
|
|
limit: this.pagination.per_page
|
|
});
|
|
|
|
// Add filters
|
|
if (this.filters.search) {
|
|
params.append('search', this.filters.search);
|
|
}
|
|
if (this.filters.status) {
|
|
params.append('status', this.filters.status);
|
|
}
|
|
if (this.filters.date_from) {
|
|
params.append('date_from', this.filters.date_from);
|
|
}
|
|
if (this.filters.date_to) {
|
|
params.append('date_to', this.filters.date_to);
|
|
}
|
|
|
|
const response = await apiClient.get(`/vendor/orders?${params.toString()}`);
|
|
|
|
this.orders = response.orders || [];
|
|
this.pagination.total = response.total || 0;
|
|
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
|
|
|
|
// Calculate stats
|
|
this.calculateStats();
|
|
|
|
vendorOrdersLog.info('Loaded orders:', this.orders.length, 'of', this.pagination.total);
|
|
} catch (error) {
|
|
vendorOrdersLog.error('Failed to load orders:', error);
|
|
this.error = error.message || 'Failed to load orders';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Calculate order statistics
|
|
*/
|
|
calculateStats() {
|
|
this.stats = {
|
|
total: this.pagination.total,
|
|
pending: this.orders.filter(o => o.status === 'pending').length,
|
|
processing: this.orders.filter(o => o.status === 'processing').length,
|
|
completed: this.orders.filter(o => ['completed', 'delivered'].includes(o.status)).length,
|
|
cancelled: this.orders.filter(o => o.status === 'cancelled').length
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Debounced search handler
|
|
*/
|
|
debouncedSearch() {
|
|
clearTimeout(this.searchTimeout);
|
|
this.searchTimeout = setTimeout(() => {
|
|
this.pagination.page = 1;
|
|
this.loadOrders();
|
|
}, 300);
|
|
},
|
|
|
|
/**
|
|
* Apply filter and reload
|
|
*/
|
|
applyFilter() {
|
|
this.pagination.page = 1;
|
|
this.loadOrders();
|
|
},
|
|
|
|
/**
|
|
* Clear all filters
|
|
*/
|
|
clearFilters() {
|
|
this.filters = {
|
|
search: '',
|
|
status: '',
|
|
date_from: '',
|
|
date_to: ''
|
|
};
|
|
this.pagination.page = 1;
|
|
this.loadOrders();
|
|
},
|
|
|
|
/**
|
|
* View order details - navigates to detail page
|
|
*/
|
|
viewOrder(order) {
|
|
window.location.href = `/vendor/${this.vendorCode}/orders/${order.id}`;
|
|
},
|
|
|
|
/**
|
|
* Open status change modal
|
|
*/
|
|
openStatusModal(order) {
|
|
this.selectedOrder = order;
|
|
this.newStatus = order.status;
|
|
this.showStatusModal = true;
|
|
},
|
|
|
|
/**
|
|
* Update order status
|
|
*/
|
|
async updateStatus() {
|
|
if (!this.selectedOrder || !this.newStatus) return;
|
|
|
|
this.saving = true;
|
|
try {
|
|
await apiClient.put(`/vendor/orders/${this.selectedOrder.id}/status`, {
|
|
status: this.newStatus
|
|
});
|
|
|
|
Utils.showToast('Order status updated', 'success');
|
|
vendorOrdersLog.info('Updated order status:', this.selectedOrder.id, this.newStatus);
|
|
|
|
this.showStatusModal = false;
|
|
this.selectedOrder = null;
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
vendorOrdersLog.error('Failed to update status:', error);
|
|
Utils.showToast(error.message || 'Failed to update status', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get status color class
|
|
*/
|
|
getStatusColor(status) {
|
|
const statusObj = this.statuses.find(s => s.value === status);
|
|
return statusObj ? statusObj.color : 'gray';
|
|
},
|
|
|
|
/**
|
|
* Get status label
|
|
*/
|
|
getStatusLabel(status) {
|
|
const statusObj = this.statuses.find(s => s.value === status);
|
|
return statusObj ? statusObj.label : status;
|
|
},
|
|
|
|
/**
|
|
* Format price for display
|
|
*/
|
|
formatPrice(cents) {
|
|
if (!cents && cents !== 0) return '-';
|
|
return new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(cents / 100);
|
|
},
|
|
|
|
/**
|
|
* Format date for display
|
|
*/
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
return new Date(dateStr).toLocaleDateString('de-DE', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Pagination: Previous page
|
|
*/
|
|
previousPage() {
|
|
if (this.pagination.page > 1) {
|
|
this.pagination.page--;
|
|
this.loadOrders();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Pagination: Next page
|
|
*/
|
|
nextPage() {
|
|
if (this.pagination.page < this.totalPages) {
|
|
this.pagination.page++;
|
|
this.loadOrders();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Pagination: Go to specific page
|
|
*/
|
|
goToPage(pageNum) {
|
|
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) {
|
|
this.pagination.page = pageNum;
|
|
this.loadOrders();
|
|
}
|
|
},
|
|
|
|
// ============================================================================
|
|
// BULK OPERATIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Toggle select all orders on current page
|
|
*/
|
|
toggleSelectAll() {
|
|
if (this.allSelected) {
|
|
this.selectedOrders = [];
|
|
} else {
|
|
this.selectedOrders = this.orders.map(o => o.id);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Toggle selection of a single order
|
|
*/
|
|
toggleSelect(orderId) {
|
|
const index = this.selectedOrders.indexOf(orderId);
|
|
if (index === -1) {
|
|
this.selectedOrders.push(orderId);
|
|
} else {
|
|
this.selectedOrders.splice(index, 1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check if order is selected
|
|
*/
|
|
isSelected(orderId) {
|
|
return this.selectedOrders.includes(orderId);
|
|
},
|
|
|
|
/**
|
|
* Clear all selections
|
|
*/
|
|
clearSelection() {
|
|
this.selectedOrders = [];
|
|
},
|
|
|
|
/**
|
|
* Open bulk status change modal
|
|
*/
|
|
openBulkStatusModal() {
|
|
if (this.selectedOrders.length === 0) return;
|
|
this.bulkStatus = '';
|
|
this.showBulkStatusModal = true;
|
|
},
|
|
|
|
/**
|
|
* Execute bulk status update
|
|
*/
|
|
async bulkUpdateStatus() {
|
|
if (this.selectedOrders.length === 0 || !this.bulkStatus) return;
|
|
|
|
this.saving = true;
|
|
try {
|
|
let successCount = 0;
|
|
for (const orderId of this.selectedOrders) {
|
|
try {
|
|
await apiClient.put(`/vendor/orders/${orderId}/status`, {
|
|
status: this.bulkStatus
|
|
});
|
|
successCount++;
|
|
} catch (error) {
|
|
vendorOrdersLog.warn(`Failed to update order ${orderId}:`, error);
|
|
}
|
|
}
|
|
Utils.showToast(`${successCount} order(s) updated to ${this.getStatusLabel(this.bulkStatus)}`, 'success');
|
|
this.showBulkStatusModal = false;
|
|
this.clearSelection();
|
|
await this.loadOrders();
|
|
} catch (error) {
|
|
vendorOrdersLog.error('Bulk status update failed:', error);
|
|
Utils.showToast(error.message || 'Failed to update orders', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Export selected orders as CSV
|
|
*/
|
|
exportSelectedOrders() {
|
|
if (this.selectedOrders.length === 0) return;
|
|
|
|
const selectedOrderData = this.orders.filter(o => this.selectedOrders.includes(o.id));
|
|
|
|
// Build CSV content
|
|
const headers = ['Order ID', 'Date', 'Customer', 'Status', 'Total'];
|
|
const rows = selectedOrderData.map(o => [
|
|
o.order_number || o.id,
|
|
this.formatDate(o.created_at),
|
|
o.customer_name || o.customer_email || '-',
|
|
this.getStatusLabel(o.status),
|
|
this.formatPrice(o.total)
|
|
]);
|
|
|
|
const csvContent = [
|
|
headers.join(','),
|
|
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
|
|
].join('\n');
|
|
|
|
// Download
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = `orders_export_${new Date().toISOString().split('T')[0]}.csv`;
|
|
link.click();
|
|
|
|
Utils.showToast(`Exported ${selectedOrderData.length} order(s)`, 'success');
|
|
}
|
|
};
|
|
}
|