// static/admin/js/orders.js /** * Admin orders management page logic * View and manage orders across all vendors */ const adminOrdersLog = window.LogConfig.loggers.adminOrders || window.LogConfig.createLogger('adminOrders', false); adminOrdersLog.info('Loading...'); function adminOrders() { adminOrdersLog.info('adminOrders() called'); return { // Inherit base layout state ...data(), // Set page identifier currentPage: 'orders', // Loading states loading: true, error: '', saving: false, // Orders data orders: [], stats: { total_orders: 0, pending_orders: 0, processing_orders: 0, shipped_orders: 0, delivered_orders: 0, cancelled_orders: 0, refunded_orders: 0, total_revenue: 0, vendors_with_orders: 0 }, // Filters filters: { search: '', vendor_id: '', status: '', channel: '' }, // Available vendors for filter dropdown vendors: [], // Pagination pagination: { page: 1, per_page: 50, total: 0, pages: 0 }, // Modal states showStatusModal: false, showDetailModal: false, selectedOrder: null, selectedOrderDetail: null, // Status update form statusForm: { status: '', tracking_number: '', reason: '' }, // 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; }, async init() { adminOrdersLog.info('Orders init() called'); // Guard against multiple initialization if (window._adminOrdersInitialized) { adminOrdersLog.warn('Already initialized, skipping'); return; } window._adminOrdersInitialized = true; // Load data in parallel await Promise.all([ this.loadStats(), this.loadVendors(), this.loadOrders() ]); adminOrdersLog.info('Orders initialization complete'); }, /** * Load order statistics */ async loadStats() { try { const response = await apiClient.get('/admin/orders/stats'); this.stats = response; adminOrdersLog.info('Loaded stats:', this.stats); } catch (error) { adminOrdersLog.error('Failed to load stats:', error); } }, /** * Load available vendors for filter */ async loadVendors() { try { const response = await apiClient.get('/admin/orders/vendors'); this.vendors = response.vendors || []; adminOrdersLog.info('Loaded vendors:', this.vendors.length); } catch (error) { adminOrdersLog.error('Failed to load vendors:', error); } }, /** * 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.vendor_id) { params.append('vendor_id', this.filters.vendor_id); } if (this.filters.status) { params.append('status', this.filters.status); } if (this.filters.channel) { params.append('channel', this.filters.channel); } const response = await apiClient.get(`/admin/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); adminOrdersLog.info('Loaded orders:', this.orders.length, 'of', this.pagination.total); } catch (error) { adminOrdersLog.error('Failed to load orders:', error); this.error = error.message || 'Failed to load orders'; } finally { this.loading = false; } }, /** * Debounced search handler */ debouncedSearch() { clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(() => { this.pagination.page = 1; this.loadOrders(); }, 300); }, /** * Refresh orders list */ async refresh() { await Promise.all([ this.loadStats(), this.loadVendors(), this.loadOrders() ]); }, /** * View order details */ async viewOrder(order) { try { const response = await apiClient.get(`/admin/orders/${order.id}`); this.selectedOrderDetail = response; this.showDetailModal = true; } catch (error) { adminOrdersLog.error('Failed to load order details:', error); Utils.showToast('Failed to load order details.', 'error'); } }, /** * Open status update modal */ openStatusModal(order) { this.selectedOrder = order; this.statusForm = { status: order.status, tracking_number: order.tracking_number || '', reason: '' }; this.showStatusModal = true; }, /** * Update order status */ async updateStatus() { if (!this.selectedOrder || this.statusForm.status === this.selectedOrder.status) return; this.saving = true; try { const payload = { status: this.statusForm.status }; if (this.statusForm.tracking_number) { payload.tracking_number = this.statusForm.tracking_number; } if (this.statusForm.reason) { payload.reason = this.statusForm.reason; } await apiClient.patch(`/admin/orders/${this.selectedOrder.id}/status`, payload); adminOrdersLog.info('Updated order status:', this.selectedOrder.id); this.showStatusModal = false; this.selectedOrder = null; Utils.showToast('Order status updated successfully.', 'success'); await this.refresh(); } catch (error) { adminOrdersLog.error('Failed to update order status:', error); Utils.showToast(error.message || 'Failed to update status.', 'error'); } finally { this.saving = false; } }, /** * Get CSS class for status badge */ getStatusClass(status) { const classes = { pending: 'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100', processing: 'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100', shipped: 'text-purple-700 bg-purple-100 dark:bg-purple-700 dark:text-purple-100', delivered: 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100', cancelled: 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100', refunded: 'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100' }; return classes[status] || 'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100'; }, /** * Format price for display */ formatPrice(price, currency = 'EUR') { if (price === null || price === undefined) return '-'; return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'EUR' }).format(price); }, /** * Format date for display */ formatDate(dateString) { if (!dateString) return '-'; const date = new Date(dateString); return date.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); }, /** * Format time for display */ formatTime(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }); }, /** * Format full date and time */ formatDateTime(dateString) { if (!dateString) return '-'; const date = new Date(dateString); return date.toLocaleString('en-GB', { day: '2-digit', month: 'short', year: '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(); } } }; }