Files
orion/static/vendor/js/orders.js
Samir Boulahtit 34d115dc58 fix: remove vendorCode from vendor API paths
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>
2026-01-02 21:49:24 +01:00

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');
}
};
}