feat(loyalty): add Chart.js visualizations to analytics page
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

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>
This commit is contained in:
2026-04-11 23:30:36 +02:00
parent 4a60d75a13
commit dd9dc04328
3 changed files with 168 additions and 1 deletions

View File

@@ -24,6 +24,12 @@ function storeLoyaltyAnalytics() {
estimated_liability_cents: 0,
},
// Advanced analytics
cohortData: { cohorts: [] },
churnData: { at_risk_count: 0, cards: [] },
revenueData: { monthly: [], by_store: [] },
revenueChart: null,
loading: false,
error: null,
@@ -56,6 +62,7 @@ function storeLoyaltyAnalytics() {
await this.loadProgram();
if (this.program) {
await this.loadStats();
this.loadAdvancedAnalytics();
}
loyaltyAnalyticsLog.info('=== LOYALTY ANALYTICS PAGE INITIALIZATION COMPLETE ===');
},
@@ -99,6 +106,72 @@ function storeLoyaltyAnalytics() {
}
},
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);