Files
orion/app/modules/loyalty/static/store/js/loyalty-analytics.js
Samir Boulahtit dd9dc04328
Some checks failed
CI / pytest (push) Failing after 2h21m5s
CI / ruff (push) Successful in 13s
CI / validate (push) Successful in 27s
CI / dependency-scanning (push) Successful in 31s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
feat(loyalty): add Chart.js visualizations to analytics page
Wire the Phase 7 analytics API endpoints into the store analytics
page with interactive visualizations:

- Revenue chart (Chart.js bar+line combo): monthly points earned as
  bars + active customers as line overlay with dual Y axes.
- At-risk members panel: ranked list of churning cards showing
  customer name and days inactive, with count badge.
- Cohort retention table: enrollment month rows × M0-M5 retention
  columns with color-coded percentage cells (green >60%, yellow
  >30%, red <30%).

Chart.js loaded on-demand via existing CDN loader with local fallback.
Data fetched in parallel via Promise.all for the 3 analytics endpoints.
All sections gracefully degrade to "not enough data" message when empty.

7 new i18n keys (EN only — FR/DE/LB translations to be added).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:30:36 +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('en-US').format(num);
}
};
}
if (!window.LogConfig.loggers.loyaltyAnalytics) {
window.LogConfig.loggers.loyaltyAnalytics = window.LogConfig.createLogger('loyaltyAnalytics');
}
loyaltyAnalyticsLog.info('Loyalty analytics module loaded');