// noqa: js-006 - async init pattern is safe, loadData has try/catch // static/admin/js/subscriptions.js // noqa: JS-003 - Uses ...baseData which is data() with safety check const subsLog = window.LogConfig?.loggers?.subscriptions || console; function adminSubscriptions() { // Get base data with safety check for standalone usage const baseData = typeof data === 'function' ? data() : {}; return { // Inherit base layout functionality from init-alpine.js ...baseData, // Page-specific state currentPage: 'subscriptions', loading: true, error: null, successMessage: null, saving: false, // Data subscriptions: [], stats: null, // Filters filters: { search: '', status: '', tier: '' }, // Pagination pagination: { page: 1, per_page: 20, total: 0, pages: 0 }, // Sorting sortBy: 'merchant_name', sortOrder: 'asc', // Modal state showModal: false, editingSub: null, formData: { tier_code: '', status: '' }, // Tiers for edit modal editTiers: [], loadingTiers: false, // Feature overrides featureOverrides: [], quantitativeFeatures: [], loadingOverrides: false, // 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() { // Guard against multiple initialization if (window._adminSubscriptionsInitialized) { subsLog.warn('Already initialized, skipping'); return; } window._adminSubscriptionsInitialized = true; subsLog.info('=== SUBSCRIPTIONS PAGE INITIALIZING ==='); // Load platform settings for rows per page if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } await this.loadStats(); await this.loadSubscriptions(); }, async refresh() { this.error = null; this.successMessage = null; await this.loadStats(); await this.loadSubscriptions(); }, async loadStats() { try { const data = await apiClient.get('/admin/subscriptions/stats'); this.stats = data; subsLog.info('Loaded subscription stats'); } catch (error) { subsLog.error('Failed to load stats:', error); } }, async loadSubscriptions() { this.loading = true; this.error = null; try { const params = new URLSearchParams(); params.append('page', this.pagination.page); params.append('per_page', this.pagination.per_page); if (this.filters.status) params.append('status', this.filters.status); if (this.filters.tier) params.append('tier', this.filters.tier); if (this.filters.search) params.append('search', this.filters.search); if (this.sortBy) params.append('sort_by', this.sortBy); if (this.sortOrder) params.append('sort_order', this.sortOrder); const data = await apiClient.get(`/admin/subscriptions?${params}`); this.subscriptions = data.subscriptions || []; this.pagination.total = data.total; this.pagination.pages = data.pages; subsLog.info(`Loaded ${this.subscriptions.length} subscriptions (total: ${this.pagination.total})`); } catch (error) { subsLog.error('Failed to load subscriptions:', error); this.error = error.message || 'Failed to load subscriptions'; } finally { this.loading = false; } }, resetFilters() { this.filters = { search: '', status: '', tier: '' }; this.pagination.page = 1; this.loadSubscriptions(); }, handleSort(key) { if (this.sortBy === key) { this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'; } else { this.sortBy = key; this.sortOrder = 'asc'; } this.pagination.page = 1; this.loadSubscriptions(); }, previousPage() { if (this.pagination.page > 1) { this.pagination.page--; this.loadSubscriptions(); } }, nextPage() { if (this.pagination.page < this.totalPages) { this.pagination.page++; this.loadSubscriptions(); } }, goToPage(pageNum) { if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) { this.pagination.page = pageNum; this.loadSubscriptions(); } }, async openEditModal(sub) { this.editingSub = sub; this.formData = { tier_code: sub.tier, status: sub.status }; this.featureOverrides = []; this.quantitativeFeatures = []; this.showModal = true; // Load tiers filtered by platform and feature overrides in parallel await Promise.all([ this.loadEditTiers(sub.platform_id), this.loadFeatureOverrides(sub.merchant_id), ]); }, async loadEditTiers(platformId) { this.loadingTiers = true; try { const url = platformId ? `/admin/subscriptions/tiers?platform_id=${platformId}` : '/admin/subscriptions/tiers'; const response = await apiClient.get(url); this.editTiers = response.tiers || []; subsLog.info('Loaded tiers for edit modal:', this.editTiers.length); } catch (error) { subsLog.error('Failed to load tiers:', error); } finally { this.loadingTiers = false; } }, closeModal() { this.showModal = false; this.editingSub = null; }, async loadFeatureOverrides(merchantId) { this.loadingOverrides = true; try { const [catalogData, overridesData] = await Promise.all([ apiClient.get('/admin/subscriptions/features/catalog'), apiClient.get(`/admin/subscriptions/features/merchants/${merchantId}/overrides`), ]); // Extract quantitative features from catalog const allFeatures = []; for (const [, features] of Object.entries(catalogData.features || {})) { for (const f of features) { if (f.feature_type === 'quantitative') { allFeatures.push(f); } } } this.quantitativeFeatures = allFeatures; // Map overrides by feature_code this.featureOverrides = (overridesData || []).map(o => ({ feature_code: o.feature_code, limit_value: o.limit_value, is_enabled: o.is_enabled })); subsLog.info(`Loaded ${allFeatures.length} quantitative features and ${this.featureOverrides.length} overrides`); } catch (error) { subsLog.error('Failed to load feature overrides:', error); } finally { this.loadingOverrides = false; } }, getOverrideValue(featureCode) { const override = this.featureOverrides.find(o => o.feature_code === featureCode); return override?.limit_value ?? ''; }, setOverrideValue(featureCode, value) { const numValue = value === '' ? null : parseInt(value, 10); const existing = this.featureOverrides.find(o => o.feature_code === featureCode); if (existing) { existing.limit_value = numValue; } else if (numValue !== null) { this.featureOverrides.push({ feature_code: featureCode, limit_value: numValue, is_enabled: true }); } }, async saveFeatureOverrides(merchantId) { // Only send overrides that have a limit_value set const entries = this.featureOverrides .filter(o => o.limit_value !== null && o.limit_value !== undefined) .map(o => ({ feature_code: o.feature_code, limit_value: o.limit_value, is_enabled: true })); if (entries.length > 0) { await apiClient.put( `/admin/subscriptions/features/merchants/${merchantId}/overrides`, entries ); } }, async saveSubscription() { if (!this.editingSub) return; this.saving = true; this.error = null; try { const payload = { ...this.formData }; await apiClient.patch( `/admin/subscriptions/merchants/${this.editingSub.merchant_id}/platforms/${this.editingSub.platform_id}`, payload ); // Save feature overrides await this.saveFeatureOverrides(this.editingSub.merchant_id); this.successMessage = `Subscription for "${this.editingSub.merchant_name}" updated`; this.closeModal(); await this.loadSubscriptions(); await this.loadStats(); } catch (error) { subsLog.error('Failed to save subscription:', error); this.error = error.message || 'Failed to update subscription'; } finally { this.saving = false; } }, formatDate(dateStr) { if (!dateStr) return '-'; return new Date(dateStr).toLocaleDateString('de-LU', { year: 'numeric', month: 'short', day: 'numeric' }); }, formatCurrency(cents) { if (cents === null || cents === undefined) return '-'; return new Intl.NumberFormat('de-LU', { style: 'currency', currency: 'EUR' }).format(cents / 100); } }; } subsLog.info('Subscriptions module loaded');