// noqa: js-006 - async init pattern is safe, loadData has try/catch // 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: [], // Selected vendor (for prominent display) selectedVendor: null, // Tom Select instance vendorSelectInstance: null, // Pagination pagination: { page: 1, per_page: 20, total: 0, pages: 0 }, // Modal states showStatusModal: false, showDetailModal: false, selectedOrder: null, selectedOrderDetail: null, // Status update form statusForm: { status: '', tracking_number: '', reason: '' }, // Mark as shipped modal showMarkAsShippedModal: false, markingAsShipped: false, shipForm: { tracking_number: '', tracking_url: '', shipping_carrier: '' }, // 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 platform settings for rows per page if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } // Initialize Tom Select for vendor filter this.initVendorSelect(); // Check localStorage for saved vendor const savedVendorId = localStorage.getItem('orders_selected_vendor_id'); if (savedVendorId) { adminOrdersLog.info('Restoring saved vendor:', savedVendorId); // Restore vendor after a short delay to ensure TomSelect is ready // restoreSavedVendor will call loadOrders() after setting the filter setTimeout(async () => { await this.restoreSavedVendor(parseInt(savedVendorId)); }, 200); // Load stats and vendors, but not orders (restoreSavedVendor will do that) await Promise.all([ this.loadStats(), this.loadVendors() ]); } else { // No saved vendor - load all data including unfiltered orders await Promise.all([ this.loadStats(), this.loadVendors(), this.loadOrders() ]); } adminOrdersLog.info('Orders initialization complete'); }, /** * Restore saved vendor from localStorage */ async restoreSavedVendor(vendorId) { try { const vendor = await apiClient.get(`/admin/vendors/${vendorId}`); if (this.vendorSelectInstance && vendor) { // Add the vendor as an option and select it this.vendorSelectInstance.addOption({ id: vendor.id, name: vendor.name, vendor_code: vendor.vendor_code }); this.vendorSelectInstance.setValue(vendor.id, true); // Set the filter state (this is the key fix!) this.selectedVendor = vendor; this.filters.vendor_id = vendor.id; adminOrdersLog.info('Restored vendor:', vendor.name); // Load orders with the vendor filter applied await this.loadOrders(); } } catch (error) { adminOrdersLog.warn('Failed to restore saved vendor, clearing localStorage:', error); localStorage.removeItem('orders_selected_vendor_id'); // Load unfiltered orders as fallback await this.loadOrders(); } }, /** * 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: 'Search vendor by name or code...', 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 `
${escape(data.name)} ${escape(data.vendor_code || '')}
`; }, item: (data, escape) => { return `
${escape(data.name)}
`; } }, onChange: (value) => { if (value) { const vendor = this.vendorSelectInstance.options[value]; this.selectedVendor = vendor; this.filters.vendor_id = value; // Save to localStorage localStorage.setItem('orders_selected_vendor_id', value.toString()); } else { this.selectedVendor = null; this.filters.vendor_id = ''; // Clear from localStorage localStorage.removeItem('orders_selected_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 = ''; // Clear from localStorage localStorage.removeItem('orders_selected_vendor_id'); this.pagination.page = 1; this.loadOrders(); }, /** * 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; } }, /** * Open mark as shipped modal */ openMarkAsShippedModal(order) { this.selectedOrder = order; this.shipForm = { tracking_number: order.tracking_number || '', tracking_url: order.tracking_url || '', shipping_carrier: order.shipping_carrier || '' }; this.showMarkAsShippedModal = true; }, /** * Mark order as shipped */ async markAsShipped() { if (!this.selectedOrder) return; this.markingAsShipped = true; try { const payload = {}; if (this.shipForm.tracking_number) { payload.tracking_number = this.shipForm.tracking_number; } if (this.shipForm.tracking_url) { payload.tracking_url = this.shipForm.tracking_url; } if (this.shipForm.shipping_carrier) { payload.shipping_carrier = this.shipForm.shipping_carrier; } await apiClient.post(`/admin/orders/${this.selectedOrder.id}/ship`, payload); adminOrdersLog.info('Marked order as shipped:', this.selectedOrder.id); this.showMarkAsShippedModal = false; this.selectedOrder = null; Utils.showToast('Order marked as shipped successfully.', 'success'); await this.refresh(); } catch (error) { adminOrdersLog.error('Failed to mark order as shipped:', error); Utils.showToast(error.message || 'Failed to mark as shipped.', 'error'); } finally { this.markingAsShipped = false; } }, /** * Download shipping label for an order */ async downloadShippingLabel(order) { try { const labelInfo = await apiClient.get(`/admin/orders/${order.id}/shipping-label`); if (labelInfo.label_url) { // Open label URL in new tab window.open(labelInfo.label_url, '_blank'); } else { Utils.showToast('No shipping label URL available for this order.', 'warning'); } } catch (error) { adminOrdersLog.error('Failed to get shipping label:', error); Utils.showToast(error.message || 'Failed to get shipping label.', 'error'); } }, /** * 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(); } } }; }