// app/modules/loyalty/static/shared/js/loyalty-program-form.js // Shared mixin for loyalty program settings forms (admin, merchant, store). /** * Factory that returns the shared Alpine.js properties and methods * for the loyalty program settings form. * * Each page spreads this into its own component and provides: * - init(), loadData(), saveSettings(), deleteProgram() * with the correct API paths and navigation. */ function createProgramFormMixin() { return { // ---- state ---- settings: { loyalty_type: 'points', stamps_target: 10, stamps_reward_description: '', stamps_reward_value_cents: null, points_per_euro: 1, welcome_bonus_points: 0, minimum_redemption_points: 100, minimum_purchase_cents: 0, points_expiration_days: null, points_rewards: [], cooldown_minutes: 15, max_daily_stamps: 5, require_staff_pin: true, card_name: '', card_color: '#4F46E5', card_secondary_color: '', logo_url: '', hero_image_url: '', terms_text: '', privacy_url: '', is_active: true, }, isNewProgram: false, saving: false, deleting: false, showDeleteModal: false, // ---- helpers ---- /** * Populate settings from an API program response object. */ populateSettings(program) { this.settings = { loyalty_type: program.loyalty_type || 'points', stamps_target: program.stamps_target || 10, stamps_reward_description: program.stamps_reward_description || '', stamps_reward_value_cents: program.stamps_reward_value_cents || null, points_per_euro: program.points_per_euro || 1, welcome_bonus_points: program.welcome_bonus_points || 0, minimum_redemption_points: program.minimum_redemption_points || 100, minimum_purchase_cents: program.minimum_purchase_cents || 0, points_expiration_days: program.points_expiration_days || null, points_rewards: (program.points_rewards || []).map(r => ({ id: r.id, name: r.name, points_required: r.points_required, description: r.description || '', is_active: r.is_active !== false, })), cooldown_minutes: program.cooldown_minutes ?? 15, max_daily_stamps: program.max_daily_stamps || 5, require_staff_pin: program.require_staff_pin !== false, card_name: program.card_name || '', card_color: program.card_color || '#4F46E5', card_secondary_color: program.card_secondary_color || '', logo_url: program.logo_url || '', hero_image_url: program.hero_image_url || '', terms_text: program.terms_text || '', privacy_url: program.privacy_url || '', is_active: program.is_active !== false, }; }, /** * Build a clean payload from settings for POST/PATCH/PUT. * Ensures rewards have IDs and cleans empty optional fields. */ buildPayload() { const payload = { ...this.settings }; // Ensure rewards have IDs payload.points_rewards = (payload.points_rewards || []).map((r, i) => ({ ...r, id: r.id || `reward_${i + 1}`, is_active: r.is_active !== false, })); // Clean empty optional fields if (!payload.stamps_reward_value_cents) payload.stamps_reward_value_cents = null; if (!payload.points_expiration_days) payload.points_expiration_days = null; if (!payload.minimum_purchase_cents) payload.minimum_purchase_cents = null; if (!payload.card_name) payload.card_name = null; if (!payload.card_secondary_color) payload.card_secondary_color = null; if (!payload.logo_url) { this.error = 'Logo URL is required for wallet integration.'; return null; } if (!payload.hero_image_url) payload.hero_image_url = null; if (!payload.terms_text) payload.terms_text = null; if (!payload.privacy_url) payload.privacy_url = null; return payload; }, addReward() { this.settings.points_rewards.push({ id: `reward_${Date.now()}`, name: '', points_required: 100, description: '', is_active: true, }); }, removeReward(index) { this.settings.points_rewards.splice(index, 1); }, confirmDelete() { this.showDeleteModal = true; }, }; }