Files
orion/app/modules/loyalty/static/store/js/loyalty-analytics.js
Samir Boulahtit bb4c400436 fix(loyalty): sweep remaining hardcoded 'en-US' in persona JS files
Follow-up to 06e59f73 which swept non-loyalty modules. The earlier
loyalty fix (dd1f9af8) only touched the shared/ factories; persona-
specific JS files in loyalty's admin/, merchant/, store/, and
storefront/ dirs were missed and still hardcoded 'en-US'.

13 occurrences across 8 files now use I18n.locale:
- admin: loyalty-analytics.js, loyalty-merchant-detail.js,
  loyalty-programs.js
- merchant: loyalty-analytics.js
- store: loyalty-analytics.js, loyalty-terminal.js
- storefront: loyalty-dashboard.js, loyalty-history.js

After this commit grep -rn "'en-US'" --include=*.js across the whole
repo returns nothing. Clearing the deck so the JS-016 rule can ship
at error severity in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:51:24 +02:00

186 lines
7.1 KiB
JavaScript

// app/modules/loyalty/static/store/js/loyalty-analytics.js
// noqa: js-006 - async init pattern is safe, loadData has try/catch
const loyaltyAnalyticsLog = window.LogConfig.loggers.loyaltyAnalytics || window.LogConfig.createLogger('loyaltyAnalytics');
function storeLoyaltyAnalytics() {
return {
...data(),
currentPage: 'analytics',
program: null,
stats: {
total_cards: 0,
active_cards: 0,
new_this_month: 0,
total_points_issued: 0,
total_points_redeemed: 0,
total_points_balance: 0,
points_issued_30d: 0,
points_redeemed_30d: 0,
transactions_30d: 0,
avg_points_per_member: 0,
estimated_liability_cents: 0,
},
// Advanced analytics
cohortData: { cohorts: [] },
churnData: { at_risk_count: 0, cards: [] },
revenueData: { monthly: [], by_store: [] },
revenueChart: null,
loading: false,
error: null,
get redemptionRate() {
if (this.stats.points_issued_30d === 0) return 0;
return Math.round((this.stats.points_redeemed_30d / this.stats.points_issued_30d) * 100);
},
get issuedPercentage() {
const total = this.stats.points_issued_30d + this.stats.points_redeemed_30d;
if (total === 0) return 50;
return Math.round((this.stats.points_issued_30d / total) * 100);
},
get redeemedPercentage() {
return 100 - this.issuedPercentage;
},
async init() {
loyaltyAnalyticsLog.info('=== LOYALTY ANALYTICS PAGE INITIALIZING ===');
if (window._loyaltyAnalyticsInitialized) return;
window._loyaltyAnalyticsInitialized = true;
// Call parent init to set storeCode from URL
const parentInit = data().init;
if (parentInit) {
await parentInit.call(this);
}
await this.loadProgram();
if (this.program) {
await this.loadStats();
this.loadAdvancedAnalytics();
}
loyaltyAnalyticsLog.info('=== LOYALTY ANALYTICS PAGE INITIALIZATION COMPLETE ===');
},
async loadProgram() {
try {
const response = await apiClient.get('/store/loyalty/program');
if (response) this.program = response;
} catch (error) {
if (error.status !== 404) throw error;
}
},
async loadStats() {
this.loading = true;
this.error = null;
try {
const response = await apiClient.get('/store/loyalty/stats');
if (response) {
this.stats = {
total_cards: response.total_cards || 0,
active_cards: response.active_cards || 0,
new_this_month: response.new_this_month || 0,
total_points_issued: response.total_points_issued || 0,
total_points_redeemed: response.total_points_redeemed || 0,
total_points_balance: response.total_points_balance || 0,
points_issued_30d: response.points_issued_30d || 0,
points_redeemed_30d: response.points_redeemed_30d || 0,
transactions_30d: response.transactions_30d || 0,
avg_points_per_member: response.avg_points_per_member || 0,
estimated_liability_cents: response.estimated_liability_cents || 0,
};
loyaltyAnalyticsLog.info('Stats loaded');
}
} catch (error) {
loyaltyAnalyticsLog.error('Failed to load stats:', error);
this.error = error.message;
} finally {
this.loading = false;
}
},
async loadAdvancedAnalytics() {
try {
const [cohort, churn, revenue] = await Promise.all([
apiClient.get('/store/loyalty/analytics/cohorts'),
apiClient.get('/store/loyalty/analytics/churn'),
apiClient.get('/store/loyalty/analytics/revenue'),
]);
if (cohort) this.cohortData = cohort;
if (churn) this.churnData = churn;
if (revenue) {
this.revenueData = revenue;
this.$nextTick(() => this.renderRevenueChart());
}
loyaltyAnalyticsLog.info('Advanced analytics loaded');
} catch (error) {
loyaltyAnalyticsLog.warn('Advanced analytics failed:', error.message);
}
},
renderRevenueChart() {
const canvas = document.getElementById('revenueChart');
if (!canvas || !window.Chart || !this.revenueData.monthly.length) return;
if (this.revenueChart) this.revenueChart.destroy();
const labels = this.revenueData.monthly.map(m => m.month);
const pointsData = this.revenueData.monthly.map(m => m.total_points_earned);
const customersData = this.revenueData.monthly.map(m => m.unique_customers);
this.revenueChart = new Chart(canvas, {
type: 'bar',
data: {
labels,
datasets: [
{
label: 'Points Earned',
data: pointsData,
backgroundColor: 'rgba(99, 102, 241, 0.7)',
borderRadius: 4,
yAxisID: 'y',
},
{
label: 'Active Customers',
data: customersData,
type: 'line',
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.3,
yAxisID: 'y1',
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: { legend: { position: 'bottom' } },
scales: {
y: { position: 'left', title: { display: true, text: 'Points' } },
y1: { position: 'right', title: { display: true, text: 'Customers' }, grid: { drawOnChartArea: false } },
},
},
});
},
formatNumber(num) {
if (num === null || num === undefined) return '0';
return new Intl.NumberFormat(I18n.locale).format(num);
}
};
}
if (!window.LogConfig.loggers.loyaltyAnalytics) {
window.LogConfig.loggers.loyaltyAnalytics = window.LogConfig.createLogger('loyaltyAnalytics');
}
loyaltyAnalyticsLog.info('Loyalty analytics module loaded');