feat(loyalty): add Chart.js visualizations to analytics page
Some checks failed
Some checks failed
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user