'use client' import { useState } from 'react' import { X, RefreshCcw } from 'lucide-react' import { toast } from 'sonner' import { logCashPayment, linkPaymentToMember } from '@/lib/actions' interface Payment { id: string platform: string amount: number from_identifier: string from_note?: string transaction_date: string link_status: string member_id?: string space_members?: { display_name: string } | null } interface Member { id: string; display_name: string; email: string } interface Integration { platform: string; is_connected: boolean; config?: Record } interface Props { payments: Payment[] members: Member[] integrations: Integration[] currentRole: string spaceId: string } const platformColors: Record = { paypal: 'text-blue-600', zeffy: 'text-purple-600', venmo: 'text-green-600', cash: 'text-muted-foreground', } const canEdit = (role: string) => ['admin', 'board', 'treasurer'].includes(role) export function PaymentsClient({ payments: initialPayments, members, integrations, currentRole, spaceId }: Props) { const [payments, setPayments] = useState(initialPayments) const [platformFilter, setPlatformFilter] = useState('') const [statusFilter, setStatusFilter] = useState('') const [showLogCash, setShowLogCash] = useState(false) const [linkingPayment, setLinkingPayment] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') // Cash form const [cashForm, setCashForm] = useState({ amount: '', from_note: '', member_id: '', date: '' }) const platforms = ['paypal', 'zeffy', 'venmo', 'cash'] const filtered = payments.filter(p => { const matchPlatform = !platformFilter || p.platform === platformFilter const matchStatus = !statusFilter || p.link_status === statusFilter return matchPlatform && matchStatus }) const byPlatform = platforms.reduce>((acc, platform) => { acc[platform] = payments.filter(p => p.platform === platform) return acc }, {}) const unlinkedCount = payments.filter(p => p.link_status === 'unlinked').length const [syncing, setSyncing] = useState(false) async function handlePayPalSync() { setSyncing(true) try { const res = await fetch('/api/paypal/sync', { method: 'POST' }) const data = await res.json() if (!res.ok) { toast.error(data.error ?? 'Sync failed'); return } toast.success(`PayPal sync complete: ${data.imported} transactions imported`) if (data.imported > 0) { // Reload page to show new transactions window.location.reload() } } catch { toast.error('PayPal sync failed') } finally { setSyncing(false) } } async function handleLogCash(e: React.FormEvent) { e.preventDefault() setLoading(true) setError('') const result = await logCashPayment({ amount: parseFloat(cashForm.amount), from_note: cashForm.from_note, member_id: cashForm.member_id || undefined, transaction_date: cashForm.date || undefined, }) if (result.error) { setError(result.error); setLoading(false); return } if (result.data) { const linkedMember = cashForm.member_id ? members.find(m => m.id === cashForm.member_id) : null setPayments(prev => [{ ...result.data as Payment, space_members: linkedMember ? { display_name: linkedMember.display_name } : null, }, ...prev]) } setShowLogCash(false) setCashForm({ amount: '', from_note: '', member_id: '', date: '' }) setLoading(false) } async function handleLink(paymentId: string, memberId: string) { setLoading(true) const result = await linkPaymentToMember(paymentId, memberId) if (!result.error) { const linkedMember = members.find(m => m.id === memberId) setPayments(prev => prev.map(p => p.id === paymentId ? { ...p, member_id: memberId, link_status: 'linked', space_members: linkedMember ? { display_name: linkedMember.display_name } : null, } : p)) setLinkingPayment(null) } setLoading(false) } return (

Payment Reconciliation

{unlinkedCount > 0 && ( {unlinkedCount} unlinked )}
{canEdit(currentRole) && (
)}
{/* Platform summary cards */}
{platforms.map(platform => { const txs = byPlatform[platform] ?? [] const total = txs.reduce((sum, t) => sum + (t.amount ?? 0), 0) const unlinked = txs.filter(t => t.link_status === 'unlinked').length const integration = integrations.find(i => i.platform === platform) return (

{platform.toUpperCase()}

{platform !== 'cash' && ( {integration?.is_connected ? 'LIVE' : 'NOT CONNECTED'} )} {platform === 'paypal' && integration?.is_connected && canEdit(currentRole) && ( )}

{'$'}{total.toFixed(0)}

{txs.length} tx{txs.length !== 1 ? 's' : ''} · {unlinked} unlinked

) })}
{/* Transactions table */}

All Transactions

{['PLATFORM', 'AMOUNT', 'FROM / NOTE', 'DATE', 'MEMBER'].map(h => ( ))} {filtered.length > 0 ? filtered.map(p => ( )) : ( )}
{h}
{p.platform?.toUpperCase()} {'$'}{Number(p.amount).toFixed(2)} {p.from_identifier}{p.from_note ? ` — "${p.from_note}"` : ''} {p.transaction_date ? new Date(p.transaction_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '—'} {p.link_status === 'linked' && p.space_members ? ( {p.space_members.display_name} ) : canEdit(currentRole) ? ( ) : ( unlinked )}
No payments recorded yet
{/* Log Cash Modal */} {showLogCash && (

Log Cash Payment

setCashForm(f => ({ ...f, amount: e.target.value }))} placeholder="50.00" className="w-full bg-background border border-border rounded px-3 py-2 font-sans text-sm text-foreground focus:outline-none focus:border-primary transition" />
setCashForm(f => ({ ...f, date: e.target.value }))} className="w-full bg-background border border-border rounded px-3 py-2 font-sans text-sm text-foreground focus:outline-none focus:border-primary" />
setCashForm(f => ({ ...f, from_note: e.target.value }))} placeholder="John Smith — March dues" className="w-full bg-background border border-border rounded px-3 py-2 font-sans text-sm text-foreground focus:outline-none focus:border-primary transition" />
{error &&

{error}

}
)} {/* Link Member Modal */} {linkingPayment && (

Link to Member

{'$'}{linkingPayment.amount} via {linkingPayment.platform?.toUpperCase()} — {linkingPayment.from_identifier}

{members.map(m => ( ))}
)}
) }