// static/admin/js/subscription-tiers.js // noqa: JS-003 - Uses ...baseData which is data() with safety check const tiersLog = window.LogConfig?.loggers?.subscriptionTiers || window.LogConfig?.createLogger?.('subscriptionTiers') || console; function adminSubscriptionTiers() { // 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: 'subscription-tiers', loading: true, error: null, successMessage: null, saving: false, // Data tiers: [], stats: null, includeInactive: false, // Feature management features: [], categories: [], featuresGrouped: {}, selectedFeatures: [], selectedTierForFeatures: null, showFeaturePanel: false, loadingFeatures: false, savingFeatures: false, // Sorting sortBy: 'display_order', sortOrder: 'asc', // Modal state showModal: false, editingTier: null, formData: { code: '', name: '', description: '', price_monthly_cents: 0, price_annual_cents: null, orders_per_month: null, products_limit: null, team_members: null, display_order: 0, stripe_product_id: '', stripe_price_monthly_id: '', features: [], is_active: true, is_public: true }, async init() { // Guard against multiple initialization if (window._adminSubscriptionTiersInitialized) { tiersLog.warn('Already initialized, skipping'); return; } window._adminSubscriptionTiersInitialized = true; tiersLog.info('=== SUBSCRIPTION TIERS PAGE INITIALIZING ==='); try { await Promise.all([ this.loadTiers(), this.loadStats(), this.loadFeatures(), this.loadCategories() ]); tiersLog.info('=== SUBSCRIPTION TIERS PAGE INITIALIZED ==='); } catch (error) { tiersLog.error('Failed to initialize subscription tiers page:', error); this.error = 'Failed to load page data. Please refresh.'; } }, async refresh() { this.error = null; this.successMessage = null; await this.loadTiers(); await this.loadStats(); }, async loadTiers() { this.loading = true; this.error = null; try { const params = new URLSearchParams(); params.append('include_inactive', this.includeInactive); 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/tiers?${params}`); this.tiers = data.tiers || []; tiersLog.info(`Loaded ${this.tiers.length} tiers`); } catch (error) { tiersLog.error('Failed to load tiers:', error); this.error = error.message || 'Failed to load subscription tiers'; } finally { this.loading = false; } }, handleSort(key) { if (this.sortBy === key) { this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'; } else { this.sortBy = key; this.sortOrder = 'asc'; } this.loadTiers(); }, async loadStats() { try { const data = await apiClient.get('/admin/subscriptions/stats'); this.stats = data; tiersLog.info('Loaded subscription stats'); } catch (error) { tiersLog.error('Failed to load stats:', error); // Non-critical, don't show error } }, openCreateModal() { this.editingTier = null; this.formData = { code: '', name: '', description: '', price_monthly_cents: 0, price_annual_cents: null, orders_per_month: null, products_limit: null, team_members: null, display_order: this.tiers.length, stripe_product_id: '', stripe_price_monthly_id: '', features: [], is_active: true, is_public: true }; this.showModal = true; }, openEditModal(tier) { this.editingTier = tier; this.formData = { code: tier.code, name: tier.name, description: tier.description || '', price_monthly_cents: tier.price_monthly_cents, price_annual_cents: tier.price_annual_cents, orders_per_month: tier.orders_per_month, products_limit: tier.products_limit, team_members: tier.team_members, display_order: tier.display_order, stripe_product_id: tier.stripe_product_id || '', stripe_price_monthly_id: tier.stripe_price_monthly_id || '', features: tier.features || [], is_active: tier.is_active, is_public: tier.is_public }; this.showModal = true; }, closeModal() { this.showModal = false; this.editingTier = null; }, async saveTier() { this.saving = true; this.error = null; try { // Clean up null values for empty strings const payload = { ...this.formData }; if (payload.price_annual_cents === '') payload.price_annual_cents = null; if (payload.orders_per_month === '') payload.orders_per_month = null; if (payload.products_limit === '') payload.products_limit = null; if (payload.team_members === '') payload.team_members = null; if (this.editingTier) { // Update existing tier await apiClient.patch(`/admin/subscriptions/tiers/${this.editingTier.code}`, payload); this.successMessage = `Tier "${payload.name}" updated successfully`; } else { // Create new tier await apiClient.post('/admin/subscriptions/tiers', payload); this.successMessage = `Tier "${payload.name}" created successfully`; } this.closeModal(); await this.loadTiers(); } catch (error) { tiersLog.error('Failed to save tier:', error); this.error = error.message || 'Failed to save tier'; } finally { this.saving = false; } }, async toggleTierStatus(tier, activate) { try { await apiClient.patch(`/admin/subscriptions/tiers/${tier.code}`, { is_active: activate }); this.successMessage = `Tier "${tier.name}" ${activate ? 'activated' : 'deactivated'}`; await this.loadTiers(); } catch (error) { tiersLog.error('Failed to toggle tier status:', error); this.error = error.message || 'Failed to update tier'; } }, formatCurrency(cents) { if (cents === null || cents === undefined) return '-'; return new Intl.NumberFormat('de-LU', { style: 'currency', currency: 'EUR' }).format(cents / 100); }, // ==================== FEATURE MANAGEMENT ==================== async loadFeatures() { try { const data = await apiClient.get('/admin/features'); this.features = data.features || []; tiersLog.info(`Loaded ${this.features.length} features`); } catch (error) { tiersLog.error('Failed to load features:', error); } }, async loadCategories() { try { const data = await apiClient.get('/admin/features/categories'); this.categories = data.categories || []; tiersLog.info(`Loaded ${this.categories.length} categories`); } catch (error) { tiersLog.error('Failed to load categories:', error); } }, groupFeaturesByCategory() { this.featuresGrouped = {}; for (const category of this.categories) { this.featuresGrouped[category] = this.features.filter(f => f.category === category); } }, async openFeaturePanel(tier) { tiersLog.info('Opening feature panel for tier:', tier.code); this.selectedTierForFeatures = tier; this.loadingFeatures = true; this.showFeaturePanel = true; try { // Load tier's current features const data = await apiClient.get(`/admin/features/tiers/${tier.code}/features`); if (data.features) { this.selectedFeatures = data.features.map(f => f.code); } else { this.selectedFeatures = tier.features || []; } } catch (error) { tiersLog.error('Failed to load tier features:', error); this.selectedFeatures = tier.features || []; } finally { this.groupFeaturesByCategory(); this.loadingFeatures = false; } }, closeFeaturePanel() { this.showFeaturePanel = false; this.selectedTierForFeatures = null; this.selectedFeatures = []; this.featuresGrouped = {}; }, toggleFeature(featureCode) { const index = this.selectedFeatures.indexOf(featureCode); if (index === -1) { this.selectedFeatures.push(featureCode); } else { this.selectedFeatures.splice(index, 1); } }, isFeatureSelected(featureCode) { return this.selectedFeatures.includes(featureCode); }, async saveFeatures() { if (!this.selectedTierForFeatures) return; tiersLog.info('Saving features for tier:', this.selectedTierForFeatures.code); this.savingFeatures = true; try { await apiClient.put( `/admin/features/tiers/${this.selectedTierForFeatures.code}/features`, { feature_codes: this.selectedFeatures } ); this.successMessage = `Features updated for ${this.selectedTierForFeatures.name}`; this.closeFeaturePanel(); await this.loadTiers(); } catch (error) { tiersLog.error('Failed to save features:', error); this.error = error.message || 'Failed to save features'; } finally { this.savingFeatures = false; } }, selectAllInCategory(category) { const categoryFeatures = this.featuresGrouped[category] || []; for (const feature of categoryFeatures) { if (!this.selectedFeatures.includes(feature.code)) { this.selectedFeatures.push(feature.code); } } }, deselectAllInCategory(category) { const categoryFeatures = this.featuresGrouped[category] || []; const codes = categoryFeatures.map(f => f.code); this.selectedFeatures = this.selectedFeatures.filter(c => !codes.includes(c)); }, allSelectedInCategory(category) { const categoryFeatures = this.featuresGrouped[category] || []; if (categoryFeatures.length === 0) return false; return categoryFeatures.every(f => this.selectedFeatures.includes(f.code)); }, formatCategoryName(category) { return category .split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } }; } tiersLog.info('Subscription tiers module loaded');