// app/modules/loyalty/static/store/js/loyalty-terminal.js // noqa: js-006 - async init pattern is safe, loadData has try/catch // Use centralized logger const loyaltyTerminalLog = window.LogConfig.loggers.loyaltyTerminal || window.LogConfig.createLogger('loyaltyTerminal'); // ============================================ // STORE LOYALTY TERMINAL FUNCTION // ============================================ function storeLoyaltyTerminal() { return { // Inherit base layout functionality ...data(), // Page identifier currentPage: 'terminal', // Program state program: null, availableRewards: [], // Customer lookup searchQuery: '', lookingUp: false, selectedCard: null, searchResults: [], showSearchDropdown: false, searchingCustomers: false, _searchTimeout: null, // Transaction inputs earnAmount: null, selectedReward: '', // PIN entry showPinEntry: false, pinDigits: '', pendingAction: null, // 'earn' or 'redeem' processing: false, // Recent transactions recentTransactions: [], // State loading: false, error: null, // Initialize async init() { loyaltyTerminalLog.info('=== LOYALTY TERMINAL INITIALIZING ==='); // Prevent multiple initializations if (window._loyaltyTerminalInitialized) { loyaltyTerminalLog.warn('Loyalty terminal already initialized, skipping...'); return; } window._loyaltyTerminalInitialized = true; // IMPORTANT: Call parent init first to set storeCode from URL const parentInit = data().init; if (parentInit) { await parentInit.call(this); } await this.loadData(); loyaltyTerminalLog.info('=== LOYALTY TERMINAL INITIALIZATION COMPLETE ==='); }, // Load initial data async loadData() { this.loading = true; this.error = null; try { await Promise.all([ this.loadProgram(), this.loadRecentTransactions() ]); } catch (error) { loyaltyTerminalLog.error('Failed to load data:', error); this.error = error.message || 'Failed to load terminal'; } finally { this.loading = false; } }, // Load program info async loadProgram() { try { loyaltyTerminalLog.info('Loading program info...'); const response = await apiClient.get('/store/loyalty/program'); if (response) { this.program = response; this.availableRewards = response.points_rewards || []; loyaltyTerminalLog.info('Program loaded:', this.program.display_name); } } catch (error) { if (error.status === 404) { loyaltyTerminalLog.info('No program configured'); this.program = null; } else { throw error; } } }, // Load recent transactions async loadRecentTransactions() { try { loyaltyTerminalLog.info('Loading recent transactions...'); const response = await apiClient.get('/store/loyalty/transactions?limit=10'); if (response && response.transactions) { this.recentTransactions = response.transactions; loyaltyTerminalLog.info(`Loaded ${this.recentTransactions.length} transactions`); } } catch (error) { loyaltyTerminalLog.warn('Failed to load transactions:', error.message); // Don't throw - transactions are optional } }, // Look up customer async lookupCustomer() { if (!this.searchQuery) return; this.lookingUp = true; this.selectedCard = null; try { loyaltyTerminalLog.info('Looking up customer:', this.searchQuery); const response = await apiClient.get(`/store/loyalty/cards/lookup?q=${encodeURIComponent(this.searchQuery)}`); if (response) { this.selectedCard = response; loyaltyTerminalLog.info('Customer found:', this.selectedCard.customer_name); this.searchQuery = ''; } } catch (error) { if (error.status === 404) { Utils.showToast(I18n.t('loyalty.store.terminal.customer_not_found'), 'warning'); } else { Utils.showToast(I18n.t('loyalty.store.terminal.error_lookup', {message: error.message}), 'error'); } loyaltyTerminalLog.error('Lookup failed:', error); } finally { this.lookingUp = false; } }, // Debounced search for autocomplete suggestions debouncedSearchCustomers() { if (this._searchTimeout) clearTimeout(this._searchTimeout); if (!this.searchQuery || this.searchQuery.length < 2) { this.searchResults = []; this.showSearchDropdown = false; return; } this._searchTimeout = setTimeout(() => this.searchCustomers(), 300); }, async searchCustomers() { this.searchingCustomers = true; try { const params = new URLSearchParams({ search: this.searchQuery, limit: '5', is_active: 'true' }); const response = await apiClient.get(`/store/loyalty/cards?${params}`); if (response && response.cards) { this.searchResults = response.cards; this.showSearchDropdown = this.searchResults.length > 0; } } catch (error) { loyaltyTerminalLog.warn('Search failed:', error.message); this.searchResults = []; this.showSearchDropdown = false; } finally { this.searchingCustomers = false; } }, // Select a customer from autocomplete dropdown async selectCustomer(card) { this.showSearchDropdown = false; this.searchResults = []; this.lookingUp = true; try { // Use the lookup endpoint to get full card details const response = await apiClient.get(`/store/loyalty/cards/lookup?q=${encodeURIComponent(card.card_number)}`); if (response) { this.selectedCard = response; this.searchQuery = ''; loyaltyTerminalLog.info('Customer selected:', this.selectedCard.customer_name); } } catch (error) { Utils.showToast(I18n.t('loyalty.store.terminal.error_lookup', {message: error.message}), 'error'); loyaltyTerminalLog.error('Lookup failed:', error); } finally { this.lookingUp = false; } }, // Clear selected customer clearCustomer() { this.selectedCard = null; this.earnAmount = null; this.selectedReward = ''; }, // Get selected reward points getSelectedRewardPoints() { if (!this.selectedReward) return 0; const reward = this.availableRewards.find(r => r.id === this.selectedReward); return reward ? reward.points_required : 0; }, // Show PIN modal showPinModal(action) { this.pendingAction = action; this.pinDigits = ''; this.showPinEntry = true; }, // PIN entry methods addPinDigit(digit) { if (this.pinDigits.length < 4) { this.pinDigits += digit.toString(); } }, removePinDigit() { this.pinDigits = this.pinDigits.slice(0, -1); }, cancelPinEntry() { this.showPinEntry = false; this.pinDigits = ''; this.pendingAction = null; }, // Submit transaction async submitTransaction() { if (this.pinDigits.length !== 4) return; this.processing = true; try { if (this.pendingAction === 'stamp') { await this.addStamp(); } else if (this.pendingAction === 'redeemStamps') { await this.redeemStamps(); } else if (this.pendingAction === 'earn') { await this.earnPoints(); } else if (this.pendingAction === 'redeem') { await this.redeemReward(); } // Close modal and refresh this.showPinEntry = false; this.pinDigits = ''; this.pendingAction = null; // Refresh customer card and transactions if (this.selectedCard) { await this.refreshCard(); } await this.loadRecentTransactions(); } catch (error) { Utils.showToast(I18n.t('loyalty.store.terminal.transaction_failed', {message: error.message}), 'error'); loyaltyTerminalLog.error('Transaction failed:', error); } finally { this.processing = false; } }, // Add stamp async addStamp() { loyaltyTerminalLog.info('Adding stamp...'); await apiClient.post('/store/loyalty/stamp', { card_id: this.selectedCard.id, staff_pin: this.pinDigits }); Utils.showToast(I18n.t('loyalty.store.terminal.stamp_added'), 'success'); }, // Redeem stamps async redeemStamps() { loyaltyTerminalLog.info('Redeeming stamps...'); await apiClient.post('/store/loyalty/stamp/redeem', { card_id: this.selectedCard.id, staff_pin: this.pinDigits }); Utils.showToast(I18n.t('loyalty.store.terminal.stamps_redeemed'), 'success'); }, // Earn points async earnPoints() { loyaltyTerminalLog.info('Earning points...', { amount: this.earnAmount }); const response = await apiClient.post('/store/loyalty/points/earn', { card_id: this.selectedCard.id, purchase_amount_cents: Math.round(this.earnAmount * 100), staff_pin: this.pinDigits }); const pointsEarned = response.points_earned || Math.floor(this.earnAmount * (this.program?.points_per_euro || 1)); Utils.showToast(I18n.t('loyalty.store.terminal.x_points_awarded', {points: pointsEarned}), 'success'); this.earnAmount = null; }, // Redeem reward async redeemReward() { const reward = this.availableRewards.find(r => r.id === this.selectedReward); if (!reward) return; loyaltyTerminalLog.info('Redeeming reward...', { reward: reward.name }); await apiClient.post('/store/loyalty/points/redeem', { card_id: this.selectedCard.id, reward_id: this.selectedReward, staff_pin: this.pinDigits }); Utils.showToast(I18n.t('loyalty.store.terminal.reward_redeemed', {name: reward.name}), 'success'); this.selectedReward = ''; }, // Refresh card data async refreshCard() { try { const response = await apiClient.get(`/store/loyalty/cards/${this.selectedCard.id}`); if (response) { this.selectedCard = response; } } catch (error) { loyaltyTerminalLog.warn('Failed to refresh card:', error.message); } }, // Format number getTransactionLabel(tx) { const type = tx.transaction_type; if (type) { return I18n.t('loyalty.transactions.' + type, {defaultValue: type.replace(/_/g, ' ')}); } return I18n.t('loyalty.common.unknown'); }, getTransactionColor(tx) { const type = tx.transaction_type || ''; if (['card_created', 'welcome_bonus', 'stamp_earned', 'points_earned'].includes(type)) { return 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100'; } if (['stamp_redeemed', 'points_redeemed'].includes(type)) { return 'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100'; } if (['stamp_voided', 'points_voided', 'points_expired', 'card_deactivated'].includes(type)) { return 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100'; } return 'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100'; }, formatNumber(num) { if (num === null || num === undefined) return '0'; return new Intl.NumberFormat('en-US').format(num); }, // Format time formatTime(dateString) { if (!dateString) return '-'; try { const date = new Date(dateString); return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); } catch (e) { return dateString; } } }; } // Register logger if (!window.LogConfig.loggers.loyaltyTerminal) { window.LogConfig.loggers.loyaltyTerminal = window.LogConfig.createLogger('loyaltyTerminal'); } loyaltyTerminalLog.info('Loyalty terminal module loaded');