feat(loyalty): align program view, edit, and analytics pages across all frontends
Some checks failed
CI / ruff (push) Successful in 11s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Standardize naming (Program for view/edit, Analytics for stats), create shared
read-only program-view partial, fix admin edit field population bug (14 missing
fields), add store Program menu item, and rename merchant Overview→Program,
Settings→Analytics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 22:51:26 +01:00
parent aefca3115e
commit eee33d6a1b
20 changed files with 674 additions and 665 deletions

View File

@@ -1,12 +1,12 @@
// app/modules/loyalty/static/store/js/loyalty-stats.js
// app/modules/loyalty/static/store/js/loyalty-analytics.js
// noqa: js-006 - async init pattern is safe, loadData has try/catch
const loyaltyStatsLog = window.LogConfig.loggers.loyaltyStats || window.LogConfig.createLogger('loyaltyStats');
const loyaltyAnalyticsLog = window.LogConfig.loggers.loyaltyAnalytics || window.LogConfig.createLogger('loyaltyAnalytics');
function storeLoyaltyStats() {
function storeLoyaltyAnalytics() {
return {
...data(),
currentPage: 'loyalty-stats',
currentPage: 'loyalty-analytics',
program: null,
@@ -27,9 +27,9 @@ function storeLoyaltyStats() {
error: null,
async init() {
loyaltyStatsLog.info('=== LOYALTY STATS PAGE INITIALIZING ===');
if (window._loyaltyStatsInitialized) return;
window._loyaltyStatsInitialized = true;
loyaltyAnalyticsLog.info('=== LOYALTY ANALYTICS PAGE INITIALIZING ===');
if (window._loyaltyAnalyticsInitialized) return;
window._loyaltyAnalyticsInitialized = true;
// IMPORTANT: Call parent init first to set storeCode from URL
const parentInit = data().init;
@@ -41,7 +41,7 @@ function storeLoyaltyStats() {
if (this.program) {
await this.loadStats();
}
loyaltyStatsLog.info('=== LOYALTY STATS PAGE INITIALIZATION COMPLETE ===');
loyaltyAnalyticsLog.info('=== LOYALTY ANALYTICS PAGE INITIALIZATION COMPLETE ===');
},
async loadProgram() {
@@ -72,10 +72,10 @@ function storeLoyaltyStats() {
transactions_30d: response.transactions_30d || 0,
avg_points_per_member: response.avg_points_per_member || 0
};
loyaltyStatsLog.info('Stats loaded');
loyaltyAnalyticsLog.info('Stats loaded');
}
} catch (error) {
loyaltyStatsLog.error('Failed to load stats:', error);
loyaltyAnalyticsLog.error('Failed to load stats:', error);
this.error = error.message;
} finally {
this.loading = false;
@@ -88,7 +88,7 @@ function storeLoyaltyStats() {
};
}
if (!window.LogConfig.loggers.loyaltyStats) {
window.LogConfig.loggers.loyaltyStats = window.LogConfig.createLogger('loyaltyStats');
if (!window.LogConfig.loggers.loyaltyAnalytics) {
window.LogConfig.loggers.loyaltyAnalytics = window.LogConfig.createLogger('loyaltyAnalytics');
}
loyaltyStatsLog.info('Loyalty stats module loaded');
loyaltyAnalyticsLog.info('Loyalty analytics module loaded');

View File

@@ -10,38 +10,16 @@ function loyaltySettings() {
return {
// Inherit base layout functionality
...data(),
...createProgramFormMixin(),
// Page identifier
currentPage: 'loyalty-settings',
currentPage: 'loyalty-program',
// State
program: null,
loading: false,
saving: false,
error: null,
isOwner: false,
// Form data
form: {
loyalty_type: 'points',
stamps_target: 10,
stamps_reward_description: 'Free item',
stamps_reward_value_cents: null,
points_per_euro: 10,
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',
logo_url: '',
terms_text: '',
},
// Initialize
async init() {
loyaltySettingsLog.info('=== LOYALTY SETTINGS INITIALIZING ===');
@@ -85,83 +63,37 @@ function loyaltySettings() {
const response = await apiClient.get('/store/loyalty/program');
if (response) {
this.program = response;
this.populateForm(response);
this.populateSettings(response);
this.isNewProgram = false;
loyaltySettingsLog.info('Program loaded:', response.display_name);
}
} catch (error) {
if (error.status === 404) {
loyaltySettingsLog.info('No program configured — showing create form');
this.program = null;
this.isNewProgram = true;
} else {
throw error;
}
}
},
populateForm(program) {
this.form.loyalty_type = program.loyalty_type || 'points';
this.form.stamps_target = program.stamps_target || 10;
this.form.stamps_reward_description = program.stamps_reward_description || 'Free item';
this.form.stamps_reward_value_cents = program.stamps_reward_value_cents || null;
this.form.points_per_euro = program.points_per_euro || 10;
this.form.welcome_bonus_points = program.welcome_bonus_points || 0;
this.form.minimum_redemption_points = program.minimum_redemption_points || 100;
this.form.minimum_purchase_cents = program.minimum_purchase_cents || 0;
this.form.points_expiration_days = program.points_expiration_days || null;
this.form.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,
}));
this.form.cooldown_minutes = program.cooldown_minutes ?? 15;
this.form.max_daily_stamps = program.max_daily_stamps || 5;
this.form.require_staff_pin = program.require_staff_pin !== false;
this.form.card_name = program.card_name || '';
this.form.card_color = program.card_color || '#4F46E5';
this.form.logo_url = program.logo_url || '';
this.form.terms_text = program.terms_text || '';
},
addReward() {
const id = 'reward_' + Date.now();
this.form.points_rewards.push({
id: id,
name: '',
points_required: 100,
description: '',
is_active: true,
});
},
async saveProgram() {
async saveSettings() {
this.saving = true;
try {
const payload = { ...this.form };
// Clean up 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.card_name) payload.card_name = null;
if (!payload.logo_url) payload.logo_url = null;
if (!payload.terms_text) payload.terms_text = null;
const payload = this.buildPayload();
let response;
if (this.program) {
// Update existing
response = await apiClient.put('/store/loyalty/program', payload);
Utils.showToast('Program updated successfully', 'success');
} else {
// Create new
if (this.isNewProgram) {
response = await apiClient.post('/store/loyalty/program', payload);
Utils.showToast('Program created successfully', 'success');
} else {
response = await apiClient.put('/store/loyalty/program', payload);
Utils.showToast('Program updated successfully', 'success');
}
this.program = response;
this.populateForm(response);
this.populateSettings(response);
this.isNewProgram = false;
loyaltySettingsLog.info('Program saved:', response.display_name);
} catch (error) {
@@ -171,6 +103,25 @@ function loyaltySettings() {
this.saving = false;
}
},
async deleteProgram() {
this.deleting = true;
try {
await apiClient.delete('/store/loyalty/program');
Utils.showToast('Loyalty program deleted', 'success');
loyaltySettingsLog.info('Program deleted');
// Redirect to terminal page
const storeCode = window.location.pathname.split('/')[2];
window.location.href = `/store/${storeCode}/loyalty/program`;
} catch (error) {
Utils.showToast(`Failed to delete: ${error.message}`, 'error');
loyaltySettingsLog.error('Delete failed:', error);
} finally {
this.deleting = false;
this.showDeleteModal = false;
}
},
};
}