import { useEffect, useState } from 'react'; import { Table, Drawer, Descriptions, Tag, Typography, Spin, Empty, Modal, Button, message } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { fetchCustomers, fetchCustomerDetail, fetchReservedItems, releaseReservedItem, CustomerSummary, CustomerDetail, ReservedItem } from './adminCustomersApi'; const { Title, Text } = Typography; export default function Customers() { const [customers, setCustomers] = useState([]); const [loading, setLoading] = useState(true); const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false); const [reservedFor, setReservedFor] = useState(null); const [reserved, setReserved] = useState([]); const [reservedLoading, setReservedLoading] = useState(false); const [releasing, setReleasing] = useState(null); function load() { return fetchCustomers().then(rows => { setCustomers(rows); setLoading(false); }); } useEffect(() => { load(); }, []); async function openDetail(id: number) { setDrawerOpen(true); setDetailLoading(true); const data = await fetchCustomerDetail(id); setDetail(data); setDetailLoading(false); } async function openReserved(customer: CustomerSummary) { setReservedFor(customer); setReservedLoading(true); try { setReserved(await fetchReservedItems(customer.id)); } catch (err) { message.error((err as Error).message); setReserved([]); } finally { setReservedLoading(false); } } async function handleRelease(item: ReservedItem) { if (!reservedFor) return; setReleasing(item.item_id); try { await releaseReservedItem(reservedFor.id, item.item_id); } catch (err) { message.error(`Couldn't release "${item.name}" — ${(err as Error).message}`); return; } finally { setReleasing(null); } message.success(`Released "${item.name}"`); // Refresh both the popup and the row count behind it, so the count can't // disagree with the list it opened from. setReserved(await fetchReservedItems(reservedFor.id)); load(); } const columns: ColumnsType = [ { title: 'Customer', dataIndex: 'email', sorter: (a, b) => a.email.localeCompare(b.email), render: (email: string, row: CustomerSummary) => (
{row.name || No name}
{email}
) }, { title: 'Verified', dataIndex: 'email_verified', filters: [{ text: 'Verified', value: true }, { text: 'Unverified', value: false }], onFilter: (value, row) => row.email_verified === value, render: (v: boolean) => {v ? 'Verified' : 'Unverified'} }, { title: 'Subscribed', dataIndex: 'marketing_consent', filters: [{ text: 'Subscribed', value: true }, { text: 'Not subscribed', value: false }], onFilter: (value, row) => row.marketing_consent === value, render: (v: boolean) => {v ? 'Yes' : 'No'} }, { title: 'Reserved', dataIndex: 'reserved_count', render: (count: number, customer: CustomerSummary) => Number(count) > 0 ? ( ) : ( 0 ) }, { title: 'Orders', dataIndex: 'order_count', sorter: (a, b) => a.order_count - b.order_count, defaultSortOrder: 'descend' }, { title: 'Total Spent', dataIndex: 'total_spent_cents', sorter: (a, b) => a.total_spent_cents - b.total_spent_cents, render: (v: number) => `$${(v / 100).toFixed(2)}` }, { title: 'Last Order', dataIndex: 'last_order_at', sorter: (a, b) => new Date(a.last_order_at || 0).getTime() - new Date(b.last_order_at || 0).getTime(), render: (v: string | null) => (v ? new Date(v).toLocaleDateString() : '—') }, { title: 'Joined', dataIndex: 'created_at', sorter: (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime(), render: (v: string) => new Date(v).toLocaleDateString() } ]; return (
Customers ({ onClick: () => openDetail(row.id), style: { cursor: 'pointer' } })} pagination={{ pageSize: 10 }} /> { setDrawerOpen(false); setDetail(null); }} width={480} > {detailLoading || !detail ? ( ) : ( <> {detail.customer.email} {detail.customer.email_verified ? 'Verified' : 'Unverified'} {detail.customer.marketing_consent ? 'Subscribed' : 'Not subscribed'} {detail.customer.marketing_consent_at && (
since {new Date(detail.customer.marketing_consent_at).toLocaleDateString()}
)}
{new Date(detail.customer.created_at).toLocaleDateString()}
Order History {detail.orders.length === 0 ? ( ) : (
`$${(v / 100).toFixed(2)}` }, { title: 'Processor', dataIndex: 'processor', render: (v: string) => {v} }, { title: 'Status', dataIndex: 'status' }, { title: 'Date', dataIndex: 'created_at', render: (v: string) => new Date(v).toLocaleDateString() } ]} /> )} )} setReservedFor(null)} footer={null} destroyOnHidden width={640} > {reservedLoading ? : null} {!reservedLoading && !reserved.length ? ( ) : null} {!reservedLoading && reserved.length > 0 && (
`$${(v / 100).toFixed(2)}` }, { title: 'Reservation expires', dataIndex: 'expires_at', render: (v: string) => new Date(v).toLocaleString() }, { title: '', render: (_: unknown, item: ReservedItem) => ( ) } ]} /> )} ); }