// app/modules/loyalty/static/shared/js/loyalty-card-detail-view.js // Shared Alpine.js data factory for loyalty card detail pages. // Used by store, merchant, and admin frontends. const loyaltyCardDetailViewLog = window.LogConfig.loggers.loyaltyCardDetailView || window.LogConfig.createLogger('loyaltyCardDetailView'); /** * Factory that returns an Alpine.js data object for a single loyalty card detail view. * * @param {Object} config * @param {string} config.apiPrefix - API path prefix, e.g. '/store/loyalty' * @param {string} config.backUrl - URL for the "back to list" navigation * @param {string} config.currentPage - Alpine currentPage identifier * @param {boolean} [config.paginate] - Enable paginated transaction history (default false, loads up to 50) * @param {Object} [config.txLabels] - Map of transaction_type -> translated label (default {}) * @param {Object} [config.txNotes] - Map of raw_note -> translated note (default {}) */ function loyaltyCardDetailView(config) { const guardKey = '_loyaltyCardDetailView_' + config.currentPage + '_initialized'; return { ...data(), currentPage: config.currentPage, // Data cardId: null, card: null, transactions: [], // Translation maps (defaults to empty so admin/merchant get raw values) txLabels: config.txLabels || {}, txNotes: config.txNotes || {}, // Pagination (always present so the shared template's pagination macro // resolves; only the wrapper that sets show_pagination renders it) pagination: { page: 1, per_page: 20, total: 0 }, // State loading: false, error: null, // Config (exposed for templates) _config: config, async init() { loyaltyCardDetailViewLog.info('=== LOYALTY CARD DETAIL VIEW INITIALIZING ===', config.currentPage); if (window[guardKey]) return; window[guardKey] = true; const parentInit = data().init; if (parentInit) { await parentInit.call(this); } this.cardId = this._extractCardIdFromUrl(); if (!this.cardId) { this.error = I18n.t('loyalty.errors.card_not_found'); loyaltyCardDetailViewLog.error('Could not extract card ID from URL'); return; } if (config.paginate && window.PlatformSettings) { try { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } catch (e) { /* keep default */ } } loyaltyCardDetailViewLog.info('Card ID extracted:', this.cardId); await this.loadData(); loyaltyCardDetailViewLog.info('=== LOYALTY CARD DETAIL VIEW INITIALIZATION COMPLETE ==='); }, /** * Extract the card ID from the current URL path. * Looks for the 'cards' segment and returns the segment immediately after it. * e.g. /store/ORION/loyalty/cards/abc123 -> 'abc123' */ _extractCardIdFromUrl() { try { const segments = window.location.pathname.split('/').filter(Boolean); const cardsIndex = segments.indexOf('cards'); if (cardsIndex !== -1 && cardsIndex + 1 < segments.length) { return segments[cardsIndex + 1]; } } catch (error) { loyaltyCardDetailViewLog.error('Error parsing URL for card ID:', error); } return null; }, async loadData() { this.loading = true; this.error = null; try { await Promise.all([ this.loadCard(), this.loadTransactions() ]); } catch (error) { loyaltyCardDetailViewLog.error('Failed to load card data:', error); this.error = error.message; } finally { this.loading = false; } }, async loadCard() { try { const response = await apiClient.get(config.apiPrefix + '/cards/' + this.cardId); if (response) { this.card = response; } } catch (error) { loyaltyCardDetailViewLog.error('Failed to load card:', error); throw error; } }, async loadTransactions(page = 1) { try { let url; if (config.paginate) { const skip = (page - 1) * this.pagination.per_page; url = config.apiPrefix + '/cards/' + this.cardId + '/transactions?skip=' + skip + '&limit=' + this.pagination.per_page; } else { url = config.apiPrefix + '/cards/' + this.cardId + '/transactions?limit=50'; } const response = await apiClient.get(url); if (response) { this.transactions = Array.isArray(response) ? response : (response.transactions || []); if (config.paginate) { this.pagination.total = response.total || 0; this.pagination.page = page; } } } catch (error) { loyaltyCardDetailViewLog.warn('Failed to load transactions:', error.message); } }, // Standard pagination interface (matches shared pagination macro). // Always present; only rendered when the template sets show_pagination. get totalPages() { return Math.max(1, Math.ceil(this.pagination.total / this.pagination.per_page)); }, get startIndex() { return this.pagination.total === 0 ? 0 : (this.pagination.page - 1) * this.pagination.per_page + 1; }, get endIndex() { return Math.min(this.pagination.page * this.pagination.per_page, this.pagination.total); }, get pageNumbers() { const pages = []; const total = this.totalPages; const current = this.pagination.page; if (total <= 7) { for (let i = 1; i <= total; i++) pages.push(i); } else { pages.push(1); if (current > 3) pages.push('...'); const start = Math.max(2, current - 1); const end = Math.min(total - 1, current + 1); for (let i = start; i <= end; i++) pages.push(i); if (current < total - 2) pages.push('...'); pages.push(total); } return pages; }, previousPage() { if (this.pagination.page > 1) this.loadTransactions(this.pagination.page - 1); }, nextPage() { if (this.pagination.page < this.totalPages) this.loadTransactions(this.pagination.page + 1); }, goToPage(p) { if (p !== '...' && p >= 1 && p <= this.totalPages) this.loadTransactions(p); }, // Formatting helpers formatNumber(num) { return num == null ? '0' : new Intl.NumberFormat(I18n.locale).format(num); }, formatDate(dateString) { if (!dateString) return 'Never'; try { return new Date(dateString).toLocaleDateString(I18n.locale, { year: 'numeric', month: 'short', day: 'numeric' }); } catch (e) { return dateString; } }, formatDateTime(dateString) { if (!dateString) return 'Never'; try { return new Date(dateString).toLocaleString(I18n.locale, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch (e) { return dateString; } } }; } if (!window.LogConfig.loggers.loyaltyCardDetailView) { window.LogConfig.loggers.loyaltyCardDetailView = window.LogConfig.createLogger('loyaltyCardDetailView'); } loyaltyCardDetailViewLog.info('Loyalty card detail view shared module loaded');