ifixkart-admin/app/(admin)/customers/page.tsx

748 lines
32 KiB
TypeScript

'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<CustomerOrder[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchHistory = async () => {
try {
const data = await apiFetch<CustomerDetailResponse>(`/api/v1/admin/customers/${customerId}`);
setOrders(data.orders || []);
} catch {
// ignore
} finally {
setLoading(false);
}
};
fetchHistory();
}, [customerId]);
if (loading) {
return (
<div className="py-6 flex justify-center items-center">
<Loader2 className="w-5 h-5 animate-spin text-primary" />
</div>
);
}
if (orders.length === 0) {
return (
<div className="py-6 text-center text-[13px] text-muted-foreground">
<Package className="w-6 h-6 mx-auto mb-2 text-muted-foreground/40" />
No orders yet
</div>
);
}
return (
<div className="mx-4 mb-4 overflow-hidden crm-radius-section border border-border">
<div className="px-4 py-2.5 border-b border-border bg-gray-50 flex justify-between items-center">
<div className="flex items-center gap-1.5 text-[13px] font-semibold text-foreground">
<History className="w-3.5 h-3.5" />
Purchase history
</div>
<span className="inline-flex items-center px-2.5 py-1 crm-radius-badge bg-primary/10 text-primary text-[11px] font-semibold">
{orders.length} orders
</span>
</div>
<table className="crm-data-table">
<thead>
<tr className="border-b border-border bg-gray-50">
<th className="py-2.5 px-4 text-[13px] font-semibold text-gray-700">Date</th>
<th className="py-2.5 px-4 text-[13px] font-semibold text-gray-700">Order</th>
<th className="py-2.5 px-4 text-[13px] font-semibold text-gray-700 text-right">Amount</th>
<th className="py-2.5 px-4 text-[13px] font-semibold text-gray-700 text-right">Status</th>
</tr>
</thead>
<tbody>
{orders.map((o) => (
<tr key={o.order_id} className="border-b border-border last:border-b-0 hover:bg-muted/10">
<td className="py-2.5 px-4 text-muted-foreground">{format(new Date(o.created_at), 'dd-MMM-yyyy')}</td>
<td className="py-2.5 px-4 font-semibold text-foreground">{o.order_no}</td>
<td className="py-2.5 px-4 text-right font-semibold text-foreground">{formatCurrency(o.final_amount)}</td>
<td className="py-2.5 px-4 text-right">
<span className="inline-flex items-center px-2.5 py-1 crm-radius-badge bg-primary/10 text-primary text-[11px] font-semibold">
{o.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default function CustomerIntelligencePage() {
const [customers, setCustomers] = useState<CustomerResponse[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [expandedRows, setExpandedRows] = useState<Set<string>>(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<InsightTab>('all');
const filterRef = useRef<HTMLDivElement>(null);
const [selectedCustomerId, setSelectedCustomerId] = useState<string | null>(null);
const [detailData, setDetailData] = useState<CustomerDetailResponse | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
const fetchCustomers = async () => {
setLoading(true);
try {
const data = await apiFetch<CustomerResponse[]>('/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<CustomerDetailResponse>(`/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 (
<div className="flex flex-col gap-5 min-w-0">
<div className="flex flex-wrap items-start justify-between gap-3 min-w-0">
<div className="min-w-0">
<div className="flex items-center gap-2">
<h1 className="text-lg font-semibold text-foreground">E-Commerce Customers</h1>
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 crm-radius-toggle bg-primary/10 text-primary text-[11px] font-semibold leading-none">
{insights.total}
</span>
</div>
</div>
<button
type="button"
onClick={fetchCustomers}
className="w-8 h-8 crm-radius-control border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center shrink-0"
title="Reload"
>
<RefreshCw className="w-3.5 h-3.5" />
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
<StatsSparklineCard
title="Total customers"
value={insights.total}
icon={Users}
tone="green"
series={datesToSparkline(allDates)}
deltaPercent={weekOverWeekChange(allDates)}
active={insightTab === 'all'}
onClick={() => setInsightTab('all')}
/>
<StatsSparklineCard
title="Loyal customers"
value={`${insights.retentionRate}%`}
icon={TrendingUp}
tone="violet"
series={datesToSparkline(loyalDates)}
deltaPercent={weekOverWeekChange(loyalDates)}
active={insightTab === 'loyal'}
onClick={() => setInsightTab('loyal')}
/>
<StatsSparklineCard
title="Recent customers"
value={insights.newThisWeek}
icon={UserPlus}
tone="orange"
series={datesToSparkline(recentDates)}
deltaPercent={weekOverWeekChange(recentDates)}
active={insightTab === 'recent'}
onClick={() => setInsightTab('recent')}
/>
<StatsSparklineCard
title="Avg sales"
value={formatCurrency(insights.avgLtv)}
icon={Zap}
tone="blue"
series={datesToSparkline(spentDates)}
deltaPercent={weekOverWeekChange(spentDates)}
active={insightTab === 'avg'}
onClick={() => setInsightTab('avg')}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 min-w-0">
<div className={`lg:col-span-2 ${dataCardShell}`}>
<div className="p-4">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2.5 min-w-0">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 min-w-0">
<div className="relative" ref={filterRef}>
<button
type="button"
onClick={() => {
setDraftFilters(activeFilters);
setShowFilterPanel((open) => !open);
}}
className={`inline-flex items-center gap-1.5 h-9 px-3 crm-radius-control border bg-card text-[13px] font-medium cursor-pointer transition-colors ${
filtersActive || showFilterPanel
? 'border-primary text-primary bg-primary/5'
: 'border-border text-foreground hover:bg-muted'
}`}
>
<Filter className="w-3.5 h-3.5" />
Filter
{filtersActive && <span className="w-1.5 h-1.5 rounded-full bg-primary" />}
<ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
</button>
{showFilterPanel && (
<div className="absolute left-0 top-full mt-1.5 z-30 w-[260px] crm-radius-section border border-border bg-card shadow-lg p-3 space-y-3">
<div>
<label className={labelClass}>Orders</label>
<CustomSelect
value={draftFilters.orders}
onChange={(orders) =>
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' },
]}
/>
</div>
<div>
<label className={labelClass}>Phone</label>
<CustomSelect
value={draftFilters.phone}
onChange={(phone) =>
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' },
]}
/>
</div>
<div>
<label className={labelClass}>Credits</label>
<CustomSelect
value={draftFilters.credits}
onChange={(credits) =>
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' },
]}
/>
</div>
<div className="flex items-center justify-end gap-2 pt-1">
<button
type="button"
onClick={() => {
setDraftFilters(EMPTY_FILTERS);
setActiveFilters(EMPTY_FILTERS);
setShowFilterPanel(false);
}}
className={secondaryButton}
>
Clear
</button>
<button
type="button"
onClick={() => {
setActiveFilters(draftFilters);
setShowFilterPanel(false);
}}
className={primaryButton}
>
Apply
</button>
</div>
</div>
)}
</div>
<div className="relative w-full sm:w-[240px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" />
<input
type="text"
placeholder="Search customers"
value={search}
onChange={(e) => 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"
/>
</div>
</div>
<ViewModeToggle value={viewMode} onChange={setViewMode} />
</div>
</div>
{loading ? (
<div className="py-16 flex justify-center border-t border-border">
<Loader2 className="w-6 h-6 animate-spin text-primary" />
</div>
) : filteredCustomers.length === 0 ? (
<div className="py-16 text-center text-[13px] text-muted-foreground border-t border-border">
{search || filtersActive ? 'No customers match your search' : 'No customers yet'}
</div>
) : viewMode === 'grid' ? (
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 p-4 border-t border-border">
{pager.items.map((customer) => (
<div key={customer.customer_id} className="crm-radius-section border border-border bg-card p-3.5 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2.5 min-w-0">
<div className="w-9 h-9 crm-radius-icon bg-primary/10 text-primary text-[12px] font-bold flex items-center justify-center shrink-0">
{customer.first_name.charAt(0).toUpperCase()}
{customer.last_name.charAt(0).toUpperCase()}
</div>
<div className="min-w-0">
<p className="text-[13px] font-semibold text-foreground truncate">
{customer.first_name} {customer.last_name}
</p>
<p className="text-[11px] text-muted-foreground truncate">{customer.email}</p>
</div>
</div>
<button
type="button"
onClick={() => handleInspectCustomer(customer.customer_id)}
className="w-8 h-8 crm-radius-control hover:bg-muted cursor-pointer flex items-center justify-center shrink-0"
title="View details"
>
<Eye className="w-3.5 h-3.5 text-muted-foreground" />
</button>
</div>
<div className="mt-3 space-y-1.5 text-[12px] text-muted-foreground">
<p className="flex items-center gap-1.5 truncate">
<Phone className="w-3.5 h-3.5 shrink-0" />
{customer.phone || '—'}
</p>
<p className="flex items-center gap-1.5 truncate">
<Mail className="w-3.5 h-3.5 shrink-0" />
{customer.email}
</p>
</div>
<div className="mt-3 flex items-center justify-between gap-2">
<span className="inline-flex items-center px-2.5 py-1 crm-radius-badge bg-warning text-white text-[11px] font-semibold">
{customer.credit_balance || 0} credits
</span>
<span className="text-[13px] font-semibold text-foreground">
{formatCurrency(customer.total_spent || 0)}
</span>
</div>
</div>
))}
</div>
) : (
<div className="overflow-x-auto border-t border-border">
<table className="crm-data-table min-w-[860px]">
<thead>
<tr className="border-b border-border bg-gray-50">
<th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Customer</th>
<th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Contact</th>
<th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Credits</th>
<th className="px-5 py-3 text-[13px] font-semibold text-gray-700">Joined</th>
<th className="px-5 py-3 text-[13px] font-semibold text-gray-700 text-right">Spent</th>
<th className="px-5 py-3 text-[13px] font-semibold text-gray-700 text-right">Actions</th>
</tr>
</thead>
<tbody>
{pager.items.map((customer) => {
const isExpanded = expandedRows.has(customer.customer_id);
return (
<React.Fragment key={customer.customer_id}>
<tr
className={`border-b border-border hover:bg-muted/10 cursor-pointer transition-colors ${
isExpanded ? 'bg-primary/5' : ''
}`}
onClick={() => toggleRow(customer.customer_id)}
>
<td className="px-5 py-3.5">
<div className="flex items-center gap-2">
<span className="text-muted-foreground">
{isExpanded ? (
<ChevronUp className="w-3.5 h-3.5" />
) : (
<ChevronDown className="w-3.5 h-3.5" />
)}
</span>
<span className="text-[13px] font-semibold text-foreground">
{customer.first_name} {customer.last_name}
</span>
</div>
</td>
<td className="px-5 py-3.5">
<div className="text-[13px] text-foreground">{customer.email}</div>
<div className="text-[11px] text-muted-foreground">{customer.phone || '—'}</div>
</td>
<td className="px-5 py-3.5">
<span className="inline-flex items-center px-2.5 py-1 crm-radius-badge bg-warning text-white text-[11px] font-semibold">
{customer.credit_balance || 0} credits
</span>
</td>
<td className="px-5 py-3.5 text-[13px] text-muted-foreground">
{format(new Date(customer.created_at), 'dd-MMM-yyyy')}
</td>
<td className="px-5 py-3.5 text-right text-[13px] font-semibold text-foreground">
{formatCurrency(customer.total_spent || 0)}
</td>
<td className="px-5 py-3.5 text-right">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleInspectCustomer(customer.customer_id);
}}
className="w-8 h-8 crm-radius-control inline-flex items-center justify-center hover:bg-muted cursor-pointer"
title="View details"
>
<Eye className="w-4 h-4 text-muted-foreground" />
</button>
</td>
</tr>
{isExpanded && (
<tr>
<td colSpan={6} className="p-0 border-b border-border bg-muted/10">
<PurchaseHistory customerId={customer.customer_id} />
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
)}
<TablePagination {...pager} />
</div>
<div className={`lg:col-span-1 ${dataCardShell} p-5 min-h-[400px]`}>
{selectedCustomerId === null ? (
<div className="h-full min-h-[320px] flex flex-col justify-center items-center text-center p-6">
<Users className="w-12 h-12 text-muted-foreground/30 mb-4" />
<p className="text-[14px] font-semibold text-foreground">Customer details</p>
<p className="text-[13px] text-muted-foreground max-w-xs mt-1">
Select a customer to view saved addresses and purchase history.
</p>
</div>
) : detailLoading ? (
<div className="h-full py-20 flex justify-center items-center">
<Loader2 className="w-6 h-6 animate-spin text-primary" />
</div>
) : detailData === null ? (
<div className="text-center py-20 text-[13px] text-destructive">Failed to load customer details.</div>
) : (
<div className="space-y-5">
<div className="flex justify-between items-start gap-3">
<div className="flex items-center gap-3 min-w-0">
<div className="w-12 h-12 crm-radius-icon bg-primary/10 text-primary flex items-center justify-center font-bold text-[15px] shrink-0">
{detailData.first_name.charAt(0).toUpperCase()}
{detailData.last_name.charAt(0).toUpperCase()}
</div>
<div className="min-w-0">
<h3 className="text-[15px] font-semibold text-foreground truncate">
{detailData.first_name} {detailData.last_name}
</h3>
<p className="text-[13px] text-muted-foreground truncate">{detailData.email}</p>
</div>
</div>
<button
type="button"
onClick={() => setSelectedCustomerId(null)}
className="w-8 h-8 crm-radius-control hover:bg-muted text-muted-foreground cursor-pointer flex items-center justify-center shrink-0"
aria-label="Close details"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="border-t border-border pt-4 space-y-4">
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase mb-2 flex items-center gap-1.5">
<MapPin className="w-3.5 h-3.5" />
Addresses ({detailData.addresses.length})
</p>
{detailData.addresses.length === 0 ? (
<p className="text-[13px] text-muted-foreground">No addresses saved.</p>
) : (
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{detailData.addresses.map((a) => (
<div
key={a.address_id}
className={`p-3 border crm-radius-section text-[13px] ${
a.is_default ? 'border-primary/30 bg-primary/5' : 'border-border'
}`}
>
<div className="flex items-center justify-between mb-1 gap-2">
<span className="font-semibold text-foreground">{a.full_name}</span>
{a.is_default && (
<span className="inline-flex items-center px-2 py-0.5 crm-radius-badge bg-primary text-white text-[11px] font-semibold">
Default
</span>
)}
</div>
<p className="text-muted-foreground">
{a.street_address}, {a.city}, {a.state} - {a.pincode}
</p>
<p className="text-muted-foreground mt-1">Phone: {a.phone}</p>
</div>
))}
</div>
)}
</div>
<div>
<p className="text-[11px] font-semibold text-muted-foreground uppercase mb-2 flex items-center gap-1.5">
<Package className="w-3.5 h-3.5" />
Orders ({detailData.orders.length})
</p>
{detailData.orders.length === 0 ? (
<p className="text-[13px] text-muted-foreground">No purchases recorded.</p>
) : (
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{detailData.orders.map((o) => (
<div
key={o.order_id}
className="p-3 border border-border crm-radius-section flex items-center justify-between gap-2"
>
<div>
<span className="text-[13px] font-semibold text-foreground block">{o.order_no}</span>
<span className="text-[12px] text-muted-foreground block">
{format(new Date(o.created_at), 'dd-MMM-yyyy')}
</span>
</div>
<div className="text-right">
<span className="text-[13px] font-semibold text-foreground block">
{formatCurrency(o.final_amount)}
</span>
<span className="text-[11px] font-semibold text-primary block">{o.status}</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
}