'use client'; import { useFormatter, useTranslations } from 'next-intl'; import { useEffect, useState } from 'react'; import { isApiClientError } from '@sport/api-client'; import { PERMISSIONS, type AdminCustomer, type AdminCustomerDetail } from '@sport/types'; import { Badge, Button, DataTable, DataTableBody, DataTableCell, DataTableHead, DataTableHeader, DataTableRow, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, EmptyState, FormError, Input, Skeleton, } from '@sport/ui'; import { useSession } from '@/features/auth/session-provider'; import { browserApi } from '@/lib/api'; import { formatDateTime, formatMoney } from '@/lib/format'; export function CustomersTable() { const t = useTranslations('customers'); const format = useFormatter(); const { can } = useSession(); const [customers, setCustomers] = useState(null); const [error, setError] = useState(null); const [query, setQuery] = useState(''); const [selectedId, setSelectedId] = useState(null); useEffect(() => { let cancelled = false; // Debounced so typing does not fire a request per keystroke; the cleanup // also discards a response whose query is already stale. const timer = setTimeout(() => { browserApi.admin .listCustomers({ perPage: 50, q: query.trim() || undefined }) .then((result) => { if (!cancelled) { setCustomers([...result.items]); setError(null); } }) .catch((caught: unknown) => { if (!cancelled) setError(isApiClientError(caught) ? caught.message : t('loadFailed')); }); }, 250); return () => { cancelled = true; clearTimeout(timer); }; }, [query, t]); if (!can(PERMISSIONS.CUSTOMER_READ)) { return

{t('noPermission')}

; } return (
setQuery(event.target.value)} placeholder={t('searchPlaceholder')} className="max-w-xs" aria-label={t('searchPlaceholder')} /> {!customers ? (
) : customers.length === 0 ? ( ) : ( {t('table.customer')} {t('table.orders')} {t('table.spend')} {t('table.marketing')} {t('table.joined')} {t('view')} {customers.map((customer) => (
{customer.displayName}
{customer.email} {customer.phone ? ` · ${customer.phone}` : ''}
{customer.orderCount} {formatMoney(customer.lifetimeSpend, format)} {customer.acceptsMarketing ? ( {t('subscribed')} ) : ( {t('notSubscribed')} )} {formatDateTime(customer.createdAt, format)}
))}
)} {selectedId ? ( setSelectedId(null)} /> ) : null}
); } function CustomerDialog({ customerId, onClose }: { customerId: string; onClose: () => void }) { const t = useTranslations('customers'); const format = useFormatter(); const [detail, setDetail] = useState(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; browserApi.admin .getCustomer(customerId) .then((result) => { if (!cancelled) setDetail(result); }) .catch((caught: unknown) => { if (!cancelled) setError(isApiClientError(caught) ? caught.message : t('loadFailed')); }); return () => { cancelled = true; }; }, [customerId, t]); return ( !open && onClose()}> {detail?.displayName ?? t('view')} {detail?.email ?? ''} {error ? (

{error}

) : !detail ? (
) : (

{t('recentOrders')}

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

{t('noOrders')}

) : (
    {detail.orders.map((order) => (
  • {order.orderNumber} {order.status} {formatDateTime(order.placedAt, format)} {formatMoney(order.total, format)}
  • ))}
)}

{t('addresses')}

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

{t('noAddresses')}

) : (
    {detail.addresses.map((address) => (
  • {address.fullName} {address.isDefaultShipping ? ( {t('default')} ) : null}

    {address.phone}
    {[address.line1, address.ward, address.district, address.province] .filter(Boolean) .join(', ')}

  • ))}
)}
)}
); } function Stat({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); }