// noqa: js-006 - async init pattern is safe, loadData has try/catch // 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: '', store_id: '' }, // Selected store (for prominent display and filtering) selectedStore: null, // Toggle status confirm state showToggleStatusConfirm: false, pendingToggleCustomer: null, // Tom Select instance storeSelectInstance: 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() { // Load i18n translations await I18n.loadModule('customers'); 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 store filter this.initStoreSelect(); // Check localStorage for saved store const savedStoreId = localStorage.getItem('customers_selected_store_id'); if (savedStoreId) { customersLog.debug('Restoring saved store:', savedStoreId); // Restore store after a short delay to ensure TomSelect is ready setTimeout(async () => { await this.restoreSavedStore(parseInt(savedStoreId)); }, 200); // Load stats but not customers (restoreSavedStore will do that) await this.loadStats(); } else { // No saved store - load all data await Promise.all([ this.loadCustomers(), this.loadStats() ]); } this.loading = false; }, /** * Restore saved store from localStorage */ async restoreSavedStore(storeId) { try { const store = await apiClient.get(`/admin/stores/${storeId}`); if (this.storeSelectInstance && store) { // Add the store as an option and select it this.storeSelectInstance.addOption({ id: store.id, name: store.name, store_code: store.store_code }); this.storeSelectInstance.setValue(store.id, true); // Set the filter state this.selectedStore = store; this.filters.store_id = store.id; customersLog.debug('Restored store:', store.name); // Load customers with the store filter applied await this.loadCustomers(); } } catch (error) { customersLog.error('Failed to restore saved store, clearing localStorage:', error); localStorage.removeItem('customers_selected_store_id'); // Load unfiltered customers as fallback await this.loadCustomers(); } }, /** * Initialize Tom Select for store autocomplete */ initStoreSelect() { const selectEl = this.$refs.storeSelect; if (!selectEl) { customersLog.warn('Store 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.initStoreSelect(), 100); return; } this.storeSelectInstance = new TomSelect(selectEl, { valueField: 'id', labelField: 'name', searchField: ['name', 'store_code'], placeholder: 'Filter by store...', allowEmptyOption: true, load: async (query, callback) => { try { const response = await apiClient.get('/admin/stores', { search: query, limit: 50 }); callback(response.stores || []); } catch (error) { customersLog.error('Failed to search stores:', error); callback([]); } }, render: { option: (data, escape) => { return `
${escape(data.name)} ${escape(data.store_code || '')}
`; }, item: (data, escape) => { return `
${escape(data.name)}
`; } }, onChange: (value) => { if (value) { const store = this.storeSelectInstance.options[value]; this.selectedStore = store; this.filters.store_id = value; // Save to localStorage localStorage.setItem('customers_selected_store_id', value.toString()); } else { this.selectedStore = null; this.filters.store_id = ''; // Clear from localStorage localStorage.removeItem('customers_selected_store_id'); } this.pagination.page = 1; this.loadCustomers(); this.loadStats(); } }); customersLog.debug('Store select initialized'); }, /** * Clear store filter */ clearStoreFilter() { if (this.storeSelectInstance) { this.storeSelectInstance.clear(); } this.selectedStore = null; this.filters.store_id = ''; // Clear from localStorage localStorage.removeItem('customers_selected_store_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.store_id) { params.append('store_id', this.filters.store_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.store_id) { params.append('store_id', this.filters.store_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) { 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); Utils.showToast(error.message || I18n.t('customers.messages.failed_to_toggle_customer_status'), 'error'); } }, /** * 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; } } }; }