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...')}

); } // Show error state with clear message and retry button if (error) { console.log('[CustomersAnalytics] Rendering error state:', error); return ( refetch()} /> ); } console.log('[CustomersAnalytics] Rendering normal content'); // Table columns const customerColumns: Column[] = [ { key: 'name', label: __('Customer'), sortable: true }, { key: 'email', label: __('Email'), sortable: true }, { key: 'orders', label: __('Orders'), sortable: true, align: 'right', }, { key: 'total_spent', label: __('Total Spent'), sortable: true, align: 'right', render: (value) => formatCurrency(value), }, { key: 'avg_order_value', label: __('Avg Order'), sortable: true, align: 'right', render: (value) => formatCurrency(value), }, { key: 'segment', label: __('Segment'), sortable: true, render: (value) => { const colors: Record = { vip: 'bg-purple-100 text-purple-800', returning: 'bg-blue-100 text-blue-800', new: 'bg-green-100 text-green-800', at_risk: 'bg-red-100 text-red-800', }; const labels: Record = { vip: __('VIP'), returning: __('Returning'), new: __('New'), at_risk: __('At Risk'), }; return ( {labels[value] || value} ); }, }, ]; return (
{/* Header */}

{__('Customers Analytics')}

{__('Customer behavior and lifetime value')}

{/* Metric Cards - Row 1: Period-based metrics */}
{/* Customer Segments - Row 2: Store-level + Period segments */}

{__('Total Customers')}

{periodMetrics.total_customers}

{__('All-time total')}

{__('Returning')}

{periodMetrics.returning_customers}

{__('In selected period')}

{__('VIP Customers')}

{__('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')}

{__('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')}

{/* Customer Acquisition Chart */} { const date = new Date(value); return `${date.getMonth() + 1}/${date.getDate()}`; }} /> { if (!active || !payload || !payload.length) return null; return (

{new Date(payload[0].payload.date).toLocaleDateString()}

{payload.map((entry: any) => (
{entry.name}: {entry.value}
))}
); }} />
{/* Two Column Layout */}
{/* Top Customers */} {/* LTV Distribution */} { if (!active || !payload || !payload.length) return null; const data = payload[0].payload; return (

{data.range}

{__('Customers')}: {data.count}

{__('Percentage')}: {data.percentage.toFixed(1)}%

); }} />
{/* All Customers Table */}
); }