import React, { useState, useMemo } from 'react'; import { BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import { Users, TrendingUp, DollarSign, ShoppingCart, UserPlus, UserCheck, Info } from 'lucide-react'; import { __ } from '@/lib/i18n'; import { formatMoney, getStoreCurrency } from '@/lib/currency'; import { useDashboardPeriod } from '@/hooks/useDashboardPeriod'; import { useCustomersAnalytics } from '@/hooks/useAnalytics'; import { ErrorCard } from '@/components/ErrorCard'; import { getPageLoadErrorMessage } from '@/lib/errorHandling'; import { StatCard } from './components/StatCard'; import { ChartCard } from './components/ChartCard'; import { DataTable, Column } from './components/DataTable'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { DUMMY_CUSTOMERS_DATA, CustomersData, TopCustomer } from './data/dummyCustomers'; export default function CustomersAnalytics() { const { period } = useDashboardPeriod(); const store = getStoreCurrency(); // Fetch real data or use dummy data based on toggle const { data, isLoading, error, refetch } = useCustomersAnalytics(DUMMY_CUSTOMERS_DATA); // ALL HOOKS MUST BE CALLED BEFORE ANY CONDITIONAL RETURNS! // Filter chart data by period const chartData = useMemo(() => { if (!data) return []; return period === 'all' ? data.acquisition_chart : data.acquisition_chart.slice(-parseInt(period)); }, [data, period]); // Calculate period metrics const periodMetrics = useMemo(() => { // Store-level data (not affected by period) const totalCustomersStoreLevel = data.overview.total_customers; // All-time total const avgLtvStoreLevel = data.overview.avg_ltv; // Lifetime value is cumulative const avgOrdersPerCustomer = data.overview.avg_orders_per_customer; // Average ratio if (period === 'all') { const totalNew = data.acquisition_chart.reduce((sum: number, d: any) => sum + d.new_customers, 0); const totalReturning = data.acquisition_chart.reduce((sum: number, d: any) => sum + d.returning_customers, 0); const totalInPeriod = totalNew + totalReturning; return { // Store-level (not affected) total_customers: totalCustomersStoreLevel, avg_ltv: avgLtvStoreLevel, avg_orders_per_customer: avgOrdersPerCustomer, // Period-based new_customers: totalNew, returning_customers: totalReturning, retention_rate: totalInPeriod > 0 ? (totalReturning / totalInPeriod) * 100 : 0, // No comparison for "all time" new_customers_change: undefined, retention_rate_change: undefined, }; } const periodData = data.acquisition_chart.slice(-parseInt(period)); const previousData = data.acquisition_chart.slice(-parseInt(period) * 2, -parseInt(period)); const totalNew = periodData.reduce((sum: number, d: any) => sum + d.new_customers, 0); const totalReturning = periodData.reduce((sum: number, d: any) => sum + d.returning_customers, 0); const totalInPeriod = totalNew + totalReturning; const prevTotalNew = previousData.reduce((sum: number, d: any) => sum + d.new_customers, 0); const prevTotalReturning = previousData.reduce((sum: number, d: any) => sum + d.returning_customers, 0); const prevTotalInPeriod = prevTotalNew + prevTotalReturning; const retentionRate = totalInPeriod > 0 ? (totalReturning / totalInPeriod) * 100 : 0; const prevRetentionRate = prevTotalInPeriod > 0 ? (prevTotalReturning / prevTotalInPeriod) * 100 : 0; return { // Store-level (not affected) total_customers: totalCustomersStoreLevel, avg_ltv: avgLtvStoreLevel, avg_orders_per_customer: avgOrdersPerCustomer, // Period-based new_customers: totalNew, returning_customers: totalReturning, retention_rate: retentionRate, // Comparisons new_customers_change: prevTotalNew > 0 ? ((totalNew - prevTotalNew) / prevTotalNew) * 100 : 0, retention_rate_change: prevRetentionRate > 0 ? ((retentionRate - prevRetentionRate) / prevRetentionRate) * 100 : 0, }; }, [data.acquisition_chart, period, data.overview]); // Format money with M/B abbreviations (translatable) const formatMoneyAxis = (value: number) => { if (value >= 1000000000) { return `${(value / 1000000000).toFixed(1)}${__('B')}`; } if (value >= 1000000) { return `${(value / 1000000).toFixed(1)}${__('M')}`; } if (value >= 1000) { return `${(value / 1000).toFixed(0)}${__('K')}`; } return value.toString(); }; // Format currency const formatCurrency = (value: number) => { return formatMoney(value, { currency: store.currency, symbol: store.symbol, thousandSep: store.thousand_sep, decimalSep: store.decimal_sep, decimals: 0, preferSymbol: true, }); }; // Format money range strings (e.g., "Rp1.000.000 - Rp5.000.000" -> "Rp1.0M - Rp5.0M") const formatMoneyRange = (rangeStr: string) => { // Extract numbers from the range string const numbers = rangeStr.match(/\d+(?:[.,]\d+)*/g); if (!numbers) return rangeStr; // Parse and format each number const formatted = numbers.map((numStr: string) => { const num = parseInt(numStr.replace(/[.,]/g, '')); return store.symbol + formatMoneyAxis(num).replace(/[^\d.KMB]/g, ''); }); // Reconstruct the range if (rangeStr.includes('-')) { return `${formatted[0]} - ${formatted[1]}`; } else if (rangeStr.startsWith('<')) { return `< ${formatted[0]}`; } else if (rangeStr.startsWith('>')) { return `> ${formatted[0]}`; } return formatted.join(' - '); }; // Filter top customers by period (for revenue in period, not LTV) const filteredTopCustomers = useMemo(() => { if (!data || !data.top_customers) return []; if (period === 'all') { return data.top_customers; // Show all-time data } // Scale customer spending by period factor for demonstration // In real implementation, this would fetch period-specific data from API const factor = parseInt(period) / 30; return data.top_customers.map((customer: any) => ({ ...customer, total_spent: Math.round(customer.total_spent * factor), orders: Math.round(customer.orders * factor), })); }, [data, period]); // Debug logging console.log('[CustomersAnalytics] State:', { isLoading, hasError: !!error, errorMessage: error?.message, hasData: !!data, dataKeys: data ? Object.keys(data) : [] }); // Show loading state if (isLoading) { console.log('[CustomersAnalytics] Rendering loading state'); return (
{__('Loading analytics...')}
{__('Customer behavior and lifetime value')}
{periodMetrics.total_customers}
{__('All-time total')}
{periodMetrics.returning_customers}
{__('In selected period')}
{__('VIP Qualification:')}
{__('Customers with 10+ orders OR lifetime value > Rp5.000.000')}
{data.segments.vip}
{((data.segments.vip / data.overview.total_customers) * 100).toFixed(1)}% {__('of total')}
{__('At Risk Qualification:')}
{__('Customers with no orders in the last 90 days')}
{data.segments.at_risk}
{((data.segments.at_risk / data.overview.total_customers) * 100).toFixed(1)}% {__('of total')}
{new Date(payload[0].payload.date).toLocaleDateString()}
{payload.map((entry: any) => ({data.range}
{__('Customers')}: {data.count}
{__('Percentage')}: {data.percentage.toFixed(1)}%