// static/admin/js/customers.js /** * Admin customer management page logic */ // Create logger for this module const customersLog = window.LogConfig?.createLogger('CUSTOMERS') || console; function adminCustomers() { return { // Inherit base layout state ...data(), // Page identifier currentPage: 'customers', // Loading states loading: true, loadingCustomers: false, // Error state error: '', // Data customers: [], stats: { total: 0, active: 0, inactive: 0, with_orders: 0, total_spent: 0, total_orders: 0, avg_order_value: 0 }, // Pagination (standard structure matching pagination macro) pagination: { page: 1, per_page: 20, total: 0, pages: 0 }, // Filters filters: { search: '', is_active: '', vendor_id: '' }, // Selected vendor (for prominent display and filtering) selectedVendor: null, // Tom Select instance vendorSelectInstance: 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() { customersLog.debug('Customers page initialized'); // 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('customers_selected_vendor_id'); if (savedVendorId) { customersLog.debug('Restoring saved vendor:', savedVendorId); // Restore vendor after a short delay to ensure TomSelect is ready setTimeout(async () => { await this.restoreSavedVendor(parseInt(savedVendorId)); }, 200); // Load stats but not customers (restoreSavedVendor will do that) await this.loadStats(); } else { // No saved vendor - load all data await Promise.all([ this.loadCustomers(), this.loadStats() ]); } this.loading = false; }, /** * 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.selectedVendor = vendor; this.filters.vendor_id = vendor.id; customersLog.debug('Restored vendor:', vendor.name); // Load customers with the vendor filter applied await this.loadCustomers(); } } catch (error) { customersLog.error('Failed to restore saved vendor, clearing localStorage:', error); localStorage.removeItem('customers_selected_vendor_id'); // Load unfiltered customers as fallback await this.loadCustomers(); } }, /** * Initialize Tom Select for vendor autocomplete */ initVendorSelect() { const selectEl = this.$refs.vendorSelect; if (!selectEl) { customersLog.warn('Vendor select element not found'); return; } // Wait for Tom Select to be available if (typeof TomSelect === 'undefined') { customersLog.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: 'Filter by vendor...', allowEmptyOption: true, load: async (query, callback) => { try { const response = await apiClient.get('/admin/vendors', { search: query, limit: 50 }); callback(response.vendors || []); } catch (error) { customersLog.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('customers_selected_vendor_id', value.toString()); } else { this.selectedVendor = null; this.filters.vendor_id = ''; // Clear from localStorage localStorage.removeItem('customers_selected_vendor_id'); } this.pagination.page = 1; this.loadCustomers(); this.loadStats(); } }); customersLog.debug('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('customers_selected_vendor_id'); this.pagination.page = 1; this.loadCustomers(); this.loadStats(); }, /** * Load customers with current filters */ async loadCustomers() { this.loadingCustomers = true; this.error = ''; try { const params = new URLSearchParams({ skip: ((this.pagination.page - 1) * this.pagination.per_page).toString(), limit: this.pagination.per_page.toString() }); if (this.filters.search) { params.append('search', this.filters.search); } if (this.filters.is_active !== '') { params.append('is_active', this.filters.is_active); } if (this.filters.vendor_id) { params.append('vendor_id', this.filters.vendor_id); } const response = await apiClient.get(`/admin/customers?${params}`); this.customers = response.customers || []; this.pagination.total = response.total || 0; this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page); } catch (error) { customersLog.error('Failed to load customers:', error); this.error = error.message || 'Failed to load customers'; this.customers = []; } finally { this.loadingCustomers = false; } }, /** * Load customer statistics */ async loadStats() { try { const params = new URLSearchParams(); if (this.filters.vendor_id) { params.append('vendor_id', this.filters.vendor_id); } const response = await apiClient.get(`/admin/customers/stats?${params}`); this.stats = response; } catch (error) { customersLog.error('Failed to load stats:', error); } }, /** * Reset pagination and reload */ async resetAndLoad() { this.pagination.page = 1; await Promise.all([ this.loadCustomers(), this.loadStats() ]); }, /** * Go to previous page */ previousPage() { if (this.pagination.page > 1) { this.pagination.page--; this.loadCustomers(); } }, /** * Go to next page */ nextPage() { if (this.pagination.page < this.totalPages) { this.pagination.page++; this.loadCustomers(); } }, /** * Go to specific page */ goToPage(pageNum) { if (typeof pageNum === 'number' && pageNum !== this.pagination.page) { this.pagination.page = pageNum; this.loadCustomers(); } }, /** * Toggle customer active status */ async toggleStatus(customer) { const action = customer.is_active ? 'deactivate' : 'activate'; if (!confirm(`Are you sure you want to ${action} this customer?`)) { return; } try { const response = await apiClient.patch(`/admin/customers/${customer.id}/toggle-status`); customer.is_active = response.is_active; // Update stats if (response.is_active) { this.stats.active++; this.stats.inactive--; } else { this.stats.active--; this.stats.inactive++; } customersLog.info(response.message); } catch (error) { customersLog.error('Failed to toggle status:', error); alert(error.message || 'Failed to toggle customer status'); } }, /** * Format currency for display */ formatCurrency(amount) { if (amount == null) return '-'; return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(amount); }, /** * Format date for display */ formatDate(dateString) { if (!dateString) return '-'; try { const date = new Date(dateString); return date.toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric' }); } catch { return dateString; } } }; }