'use client'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Users, Search, Eye, Loader2, MapPin, Package, X, TrendingUp, UserPlus, Zap, ChevronDown, ChevronUp, History, Filter, Mail, Phone, RefreshCw, } from 'lucide-react'; import { apiFetch } from '@/services/api/client'; import { format } from 'date-fns'; import { toast } from 'sonner'; import { formatCurrency } from '@/lib/utils'; import { ViewModeToggle } from '@/components/ui/ViewModeToggle'; import { CustomSelect } from '@/components/ui/CustomSelect'; import { TablePagination, useClientPagination } from '@/components/ui/TablePagination'; import { StatsSparklineCard, datesToSparkline, weekOverWeekChange, } from '@/components/ui/StatsSparklineCard'; interface CustomerAddress { address_id: string; address_type: string; full_name: string; phone: string; street_address: string; city: string; state: string; pincode: string; is_default: boolean; } interface CustomerOrder { order_id: string; order_no: string; final_amount: number; status: string; created_at: string; items_count?: number; } interface CustomerResponse { customer_id: string; email: string; first_name: string; last_name: string; phone: string | null; created_at: string; credit_balance?: number; total_spent?: number; total_orders?: number; } interface CustomerDetailResponse extends CustomerResponse { profile_picture: string | null; addresses: CustomerAddress[]; orders: CustomerOrder[]; } type OrderFilter = 'all' | 'has_orders' | 'no_orders'; type PhoneFilter = 'all' | 'has_phone' | 'no_phone'; type CreditFilter = 'all' | 'has_credits' | 'no_credits'; type InsightTab = 'all' | 'loyal' | 'recent' | 'avg'; const EMPTY_FILTERS = { orders: 'all' as OrderFilter, phone: 'all' as PhoneFilter, credits: 'all' as CreditFilter, }; const WEEK_MS = 7 * 86400000; function isNewThisWeek(date: string) { const t = new Date(date).getTime(); return Number.isFinite(t) && Date.now() - t < WEEK_MS; } const PurchaseHistory = ({ customerId }: { customerId: string }) => { const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchHistory = async () => { try { const data = await apiFetch(`/api/v1/admin/customers/${customerId}`); setOrders(data.orders || []); } catch { // ignore } finally { setLoading(false); } }; fetchHistory(); }, [customerId]); if (loading) { return (
); } if (orders.length === 0) { return (
No orders yet
); } return (
Purchase history
{orders.length} orders
{orders.map((o) => ( ))}
Date Order Amount Status
{format(new Date(o.created_at), 'dd-MMM-yyyy')} {o.order_no} {formatCurrency(o.final_amount)} {o.status}
); }; export default function CustomerIntelligencePage() { const [customers, setCustomers] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [expandedRows, setExpandedRows] = useState>(new Set()); const [viewMode, setViewMode] = useState<'table' | 'grid'>('table'); const [showFilterPanel, setShowFilterPanel] = useState(false); const [draftFilters, setDraftFilters] = useState(EMPTY_FILTERS); const [activeFilters, setActiveFilters] = useState(EMPTY_FILTERS); const [insightTab, setInsightTab] = useState('all'); const filterRef = useRef(null); const [selectedCustomerId, setSelectedCustomerId] = useState(null); const [detailData, setDetailData] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const fetchCustomers = async () => { setLoading(true); try { const data = await apiFetch('/api/v1/admin/customers'); setCustomers(data); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to fetch customers'; toast.error(message); } finally { setLoading(false); } }; useEffect(() => { fetchCustomers(); }, []); useEffect(() => { if (!showFilterPanel) return; const onPointerDown = (event: MouseEvent) => { if (!filterRef.current?.contains(event.target as Node)) { setShowFilterPanel(false); } }; document.addEventListener('mousedown', onPointerDown); return () => document.removeEventListener('mousedown', onPointerDown); }, [showFilterPanel]); const handleInspectCustomer = async (id: string) => { setSelectedCustomerId(id); setDetailLoading(true); setDetailData(null); try { const data = await apiFetch(`/api/v1/admin/customers/${id}`); setDetailData(data); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to load customer details'; toast.error(message); setSelectedCustomerId(null); } finally { setDetailLoading(false); } }; const toggleRow = (id: string) => { const next = new Set(expandedRows); if (next.has(id)) next.delete(id); else next.add(id); setExpandedRows(next); }; const loyalCustomers = useMemo( () => customers.filter((c) => (c.total_orders || 0) > 0), [customers] ); const recentCustomers = useMemo( () => customers.filter((c) => isNewThisWeek(c.created_at)), [customers] ); const allDates = useMemo(() => customers.map((c) => c.created_at), [customers]); const loyalDates = useMemo(() => loyalCustomers.map((c) => c.created_at), [loyalCustomers]); const recentDates = useMemo(() => recentCustomers.map((c) => c.created_at), [recentCustomers]); const spentDates = useMemo( () => customers.filter((c) => (c.total_spent || 0) > 0).map((c) => c.created_at), [customers] ); const insights = useMemo(() => { const total = customers.length; const totalSpent = customers.reduce((sum, c) => sum + (c.total_spent || 0), 0); const avgSpent = total > 0 ? totalSpent / total : 0; const loyal = loyalCustomers.length; return { total, loyal, retentionRate: total > 0 ? Math.round((loyal / total) * 100) : 0, newThisWeek: recentCustomers.length, avgLtv: avgSpent, }; }, [customers, loyalCustomers.length, recentCustomers.length]); const filteredCustomers = useMemo(() => { const q = search.trim().toLowerCase(); return customers.filter((c) => { const fullName = `${c.first_name} ${c.last_name}`.toLowerCase(); const searchMatch = !q || c.first_name.toLowerCase().includes(q) || c.last_name.toLowerCase().includes(q) || fullName.includes(q) || c.email.toLowerCase().includes(q) || (c.phone || '').toLowerCase().includes(q); const orderCount = c.total_orders || 0; const ordersMatch = activeFilters.orders === 'all' || (activeFilters.orders === 'has_orders' && orderCount > 0) || (activeFilters.orders === 'no_orders' && orderCount === 0); const hasPhone = Boolean(c.phone && c.phone.trim()); const phoneMatch = activeFilters.phone === 'all' || (activeFilters.phone === 'has_phone' && hasPhone) || (activeFilters.phone === 'no_phone' && !hasPhone); const credits = c.credit_balance || 0; const creditsMatch = activeFilters.credits === 'all' || (activeFilters.credits === 'has_credits' && credits > 0) || (activeFilters.credits === 'no_credits' && credits === 0); const tabMatch = insightTab === 'all' || insightTab === 'avg' || (insightTab === 'loyal' && orderCount > 0) || (insightTab === 'recent' && isNewThisWeek(c.created_at)); return searchMatch && ordersMatch && phoneMatch && creditsMatch && tabMatch; }); }, [customers, search, activeFilters, insightTab]); const pager = useClientPagination(filteredCustomers); const filtersActive = activeFilters.orders !== 'all' || activeFilters.phone !== 'all' || activeFilters.credits !== 'all'; const dataCardShell = 'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-hidden min-w-0'; const labelClass = 'block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5'; const primaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50'; const secondaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50'; return (

E-Commerce Customers

{insights.total}
setInsightTab('all')} /> setInsightTab('loyal')} /> setInsightTab('recent')} /> setInsightTab('avg')} />
{showFilterPanel && (
setDraftFilters((prev) => ({ ...prev, orders: orders as OrderFilter })) } aria-label="Orders" options={[ { value: 'all', label: 'All' }, { value: 'has_orders', label: 'Has orders' }, { value: 'no_orders', label: 'No orders' }, ]} />
setDraftFilters((prev) => ({ ...prev, phone: phone as PhoneFilter })) } aria-label="Phone" options={[ { value: 'all', label: 'All' }, { value: 'has_phone', label: 'Has phone' }, { value: 'no_phone', label: 'No phone' }, ]} />
setDraftFilters((prev) => ({ ...prev, credits: credits as CreditFilter })) } aria-label="Credits" options={[ { value: 'all', label: 'All' }, { value: 'has_credits', label: 'Has credits' }, { value: 'no_credits', label: 'No credits' }, ]} />
)}
setSearch(e.target.value)} className="w-full h-9 pl-8 pr-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors" />
{loading ? (
) : filteredCustomers.length === 0 ? (
{search || filtersActive ? 'No customers match your search' : 'No customers yet'}
) : viewMode === 'grid' ? (
{pager.items.map((customer) => (
{customer.first_name.charAt(0).toUpperCase()} {customer.last_name.charAt(0).toUpperCase()}

{customer.first_name} {customer.last_name}

{customer.email}

{customer.phone || '—'}

{customer.email}

{customer.credit_balance || 0} credits {formatCurrency(customer.total_spent || 0)}
))}
) : (
{pager.items.map((customer) => { const isExpanded = expandedRows.has(customer.customer_id); return ( toggleRow(customer.customer_id)} > {isExpanded && ( )} ); })}
Customer Contact Credits Joined Spent Actions
{isExpanded ? ( ) : ( )} {customer.first_name} {customer.last_name}
{customer.email}
{customer.phone || '—'}
{customer.credit_balance || 0} credits {format(new Date(customer.created_at), 'dd-MMM-yyyy')} {formatCurrency(customer.total_spent || 0)}
)}
{selectedCustomerId === null ? (

Customer details

Select a customer to view saved addresses and purchase history.

) : detailLoading ? (
) : detailData === null ? (
Failed to load customer details.
) : (
{detailData.first_name.charAt(0).toUpperCase()} {detailData.last_name.charAt(0).toUpperCase()}

{detailData.first_name} {detailData.last_name}

{detailData.email}

Addresses ({detailData.addresses.length})

{detailData.addresses.length === 0 ? (

No addresses saved.

) : (
{detailData.addresses.map((a) => (
{a.full_name} {a.is_default && ( Default )}

{a.street_address}, {a.city}, {a.state} - {a.pincode}

Phone: {a.phone}

))}
)}

Orders ({detailData.orders.length})

{detailData.orders.length === 0 ? (

No purchases recorded.

) : (
{detailData.orders.map((o) => (
{o.order_no} {format(new Date(o.created_at), 'dd-MMM-yyyy')}
{formatCurrency(o.final_amount)} {o.status}
))}
)}
)}
); }