// app/modules/loyalty/static/admin/js/loyalty-merchant-detail.js // noqa: js-006 - async init pattern is safe, loadData has try/catch // Use centralized logger const loyaltyMerchantDetailLog = window.LogConfig.loggers.loyaltyMerchantDetail || window.LogConfig.createLogger('loyaltyMerchantDetail'); // ============================================ // LOYALTY MERCHANT DETAIL FUNCTION // ============================================ function adminLoyaltyMerchantDetail() { return { // Inherit base layout functionality ...data(), // Page identifier for sidebar active state currentPage: 'loyalty-programs', // Merchant ID from URL merchantId: null, // Merchant data merchant: null, program: null, stats: { total_cards: 0, active_cards: 0, total_points_issued: 0, total_points_redeemed: 0, points_issued_30d: 0, points_redeemed_30d: 0, transactions_30d: 0 }, settings: null, locations: [], // Transaction categories selectedCategoryStoreId: '', storeCategories: [], showAddCategory: false, newCategoryName: '', newCategoryTranslations: { fr: '', de: '', lb: '' }, viewingCategoryId: null, editingCategoryId: null, editCategoryData: { name: '', translations: { fr: '', de: '', lb: '' } }, // State loading: false, error: null, showDeleteModal: false, // Initialize async init() { loyaltyMerchantDetailLog.info('=== LOYALTY MERCHANT DETAIL PAGE INITIALIZING ==='); // Prevent multiple initializations if (window._loyaltyMerchantDetailInitialized) { loyaltyMerchantDetailLog.warn('Loyalty merchant detail page already initialized, skipping...'); return; } window._loyaltyMerchantDetailInitialized = true; // Extract merchant ID from URL const pathParts = window.location.pathname.split('/'); const merchantsIndex = pathParts.indexOf('merchants'); if (merchantsIndex !== -1 && pathParts[merchantsIndex + 1]) { this.merchantId = parseInt(pathParts[merchantsIndex + 1]); } if (!this.merchantId) { this.error = 'Invalid merchant ID'; loyaltyMerchantDetailLog.error('Could not extract merchant ID from URL'); return; } loyaltyMerchantDetailLog.info('Merchant ID:', this.merchantId); loyaltyMerchantDetailLog.group('Loading merchant loyalty data'); await this.loadMerchantData(); loyaltyMerchantDetailLog.groupEnd(); loyaltyMerchantDetailLog.info('=== LOYALTY MERCHANT DETAIL PAGE INITIALIZATION COMPLETE ==='); }, // Load all merchant data async loadMerchantData() { this.loading = true; this.error = null; try { // Load merchant info await this.loadMerchant(); // Load loyalty-specific data in parallel await Promise.all([ this.loadStats(), this.loadSettings(), this.loadLocations() ]); } catch (error) { loyaltyMerchantDetailLog.error('Failed to load merchant data:', error); this.error = error.message || 'Failed to load merchant loyalty data'; } finally { this.loading = false; } }, // Load merchant basic info async loadMerchant() { try { loyaltyMerchantDetailLog.info('Fetching merchant info...'); // Get merchant from tenancy API const response = await apiClient.get(`/admin/merchants/${this.merchantId}`); if (response) { this.merchant = response; loyaltyMerchantDetailLog.info('Merchant loaded:', this.merchant.name); } } catch (error) { loyaltyMerchantDetailLog.error('Failed to load merchant:', error); throw error; } }, // Load merchant loyalty stats async loadStats() { try { loyaltyMerchantDetailLog.info('Fetching merchant loyalty stats...'); const response = await apiClient.get(`/admin/loyalty/merchants/${this.merchantId}/stats`); if (response) { this.stats = { total_cards: response.total_cards || 0, active_cards: response.active_cards || 0, total_points_issued: response.total_points_issued || 0, total_points_redeemed: response.total_points_redeemed || 0, points_issued_30d: response.points_issued_30d || 0, points_redeemed_30d: response.points_redeemed_30d || 0, transactions_30d: response.transactions_30d || 0 }; // Also get program info from stats response if (response.program) { this.program = response.program; } // Get location breakdown if (response.locations) { this.locations = response.locations; } loyaltyMerchantDetailLog.info('Stats loaded:', this.stats); } } catch (error) { loyaltyMerchantDetailLog.warn('Failed to load stats (merchant may not have loyalty program):', error.message); // Don't throw - stats might fail if no program exists } }, // Load merchant loyalty settings async loadSettings() { try { loyaltyMerchantDetailLog.info('Fetching merchant loyalty settings...'); const response = await apiClient.get(`/admin/loyalty/merchants/${this.merchantId}/settings`); if (response) { this.settings = response; loyaltyMerchantDetailLog.info('Settings loaded:', this.settings); } } catch (error) { loyaltyMerchantDetailLog.warn('Failed to load settings:', error.message); // Don't throw - settings might not exist yet } }, // Load location breakdown async loadLocations() { try { loyaltyMerchantDetailLog.info('Fetching location breakdown...'); // This data comes with stats, but could be a separate endpoint // For now, stats endpoint should return locations array } catch (error) { loyaltyMerchantDetailLog.warn('Failed to load locations:', error.message); } }, // Create a default program for this merchant async createProgram() { try { const data = { loyalty_type: 'points', points_per_euro: 1, welcome_bonus_points: 0, minimum_redemption_points: 100, card_name: this.merchant?.name ? this.merchant.name + ' Loyalty' : 'Loyalty Program', is_active: true }; const response = await apiClient.post(`/admin/loyalty/merchants/${this.merchantId}/program`, data); this.program = response; Utils.showToast(I18n.t('loyalty.toasts.program_created'), 'success'); loyaltyMerchantDetailLog.info('Program created for merchant', this.merchantId); // Reload stats await this.loadStats(); } catch (error) { Utils.showToast(I18n.t('loyalty.toasts.create_failed', {message: error.message}), 'error'); loyaltyMerchantDetailLog.error('Failed to create program:', error); } }, // Toggle program active/inactive async toggleActive() { if (!this.program) return; const action = this.program.is_active ? 'deactivate' : 'activate'; try { const response = await apiClient.post(`/admin/loyalty/programs/${this.program.id}/${action}`); this.program.is_active = response.is_active; Utils.showToast(I18n.t(`loyalty.toasts.program_${action}d`), 'success'); loyaltyMerchantDetailLog.info(`Program ${action}d`); } catch (error) { Utils.showToast(I18n.t(`loyalty.toasts.${action}_failed`, {message: error.message}), 'error'); loyaltyMerchantDetailLog.error(`Failed to ${action} program:`, error); } }, // Show delete confirmation confirmDeleteProgram() { this.showDeleteModal = true; }, // Delete the program async deleteProgram() { if (!this.program) return; try { await apiClient.delete(`/admin/loyalty/programs/${this.program.id}`); this.program = null; this.showDeleteModal = false; Utils.showToast(I18n.t('loyalty.toasts.program_deleted'), 'success'); loyaltyMerchantDetailLog.info('Program deleted'); // Reload stats await this.loadStats(); } catch (error) { Utils.showToast(I18n.t('loyalty.toasts.delete_failed', {message: error.message}), 'error'); loyaltyMerchantDetailLog.error('Failed to delete program:', error); this.showDeleteModal = false; } }, // Format date for display formatDate(dateString) { if (!dateString) return 'N/A'; try { const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } catch (e) { loyaltyMerchantDetailLog.error('Date parsing error:', e); return dateString; } }, // Format number with thousands separator formatNumber(num) { if (num === null || num === undefined) return '0'; return new Intl.NumberFormat('en-US').format(num); }, // Transaction categories async loadCategoriesForStore() { if (!this.selectedCategoryStoreId) { this.storeCategories = []; return; } try { const response = await apiClient.get(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories`); this.storeCategories = response?.categories || []; } catch (error) { loyaltyMerchantDetailLog.warn('Failed to load categories:', error.message); this.storeCategories = []; } }, async createCategory() { if (!this.newCategoryName || !this.selectedCategoryStoreId) return; try { // Build translations dict (only include non-empty values) const translations = {}; if (this.newCategoryName) translations.en = this.newCategoryName; for (const [lang, val] of Object.entries(this.newCategoryTranslations)) { if (val) translations[lang] = val; } await apiClient.post(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories`, { name: this.newCategoryName, name_translations: Object.keys(translations).length > 0 ? translations : null, display_order: this.storeCategories.length, }); this.newCategoryName = ''; this.newCategoryTranslations = { fr: '', de: '', lb: '' }; this.showAddCategory = false; await this.loadCategoriesForStore(); Utils.showToast('Category created', 'success'); } catch (error) { Utils.showToast(error.message || 'Failed to create category', 'error'); } }, startEditCategory(cat) { this.editingCategoryId = cat.id; this.editCategoryData = { name: cat.name, translations: { fr: cat.name_translations?.fr || '', de: cat.name_translations?.de || '', lb: cat.name_translations?.lb || '', }, }; }, async saveEditCategory(catId) { if (!this.editCategoryData.name) return; try { const translations = { en: this.editCategoryData.name }; for (const [lang, val] of Object.entries(this.editCategoryData.translations)) { if (val) translations[lang] = val; } await apiClient.patch(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories/${catId}`, { name: this.editCategoryData.name, name_translations: Object.keys(translations).length > 0 ? translations : null, }); this.editingCategoryId = null; await this.loadCategoriesForStore(); Utils.showToast('Category updated', 'success'); } catch (error) { Utils.showToast(error.message || 'Failed to update category', 'error'); } }, async toggleCategoryActive(cat) { try { await apiClient.patch(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories/${cat.id}`, { is_active: !cat.is_active, }); await this.loadCategoriesForStore(); } catch (error) { Utils.showToast(error.message || 'Failed to update category', 'error'); } }, async deleteCategory(catId) { if (!confirm('Delete this category?')) return; try { await apiClient.delete(`/admin/loyalty/stores/${this.selectedCategoryStoreId}/categories/${catId}`); await this.loadCategoriesForStore(); Utils.showToast('Category deleted', 'success'); } catch (error) { Utils.showToast(error.message || 'Failed to delete category', 'error'); } } }; } // Register logger for configuration if (!window.LogConfig.loggers.loyaltyMerchantDetail) { window.LogConfig.loggers.loyaltyMerchantDetail = window.LogConfig.createLogger('loyaltyMerchantDetail'); } loyaltyMerchantDetailLog.info('Loyalty merchant detail module loaded');