'use client' import { useState } from 'react' import { Plus, X, ChevronDown, Users } from 'lucide-react' import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty' import { addMember, updateMember, approveMember, removeMember, assignAreaLead } from '@/lib/actions' import { toast } from 'sonner' import { useRouter } from 'next/navigation' import type { Tables } from '@/types/database' import { PageTitle } from '@/components/ui/page-title' import { useConfirm } from '@/components/ui/confirm' type Member = Tables<'space_members'> type AreaLeadRole = { id: string; area_name: string; lead_id: string | null } interface Props { members: Member[] currentRole: string areaLeadRoles?: AreaLeadRole[] } const TIER_COLORS: Record = { plus: 'text-blue-600 bg-blue-50 border-blue-200', basic: 'text-muted-foreground bg-muted border-border', associate: 'text-purple-600 bg-purple-50 border-purple-200', admin: 'text-primary bg-primary/5 border-primary/20', } const isAdmin = (role: string) => role === 'admin' || role === 'board' export function MembersClient({ members: initialMembers, currentRole, areaLeadRoles = [] }: Props) { const router = useRouter() const confirm = useConfirm() const [members, setMembers] = useState(initialMembers) async function makeAreaLead(memberId: string, areaLeadRoleId: string) { if (!areaLeadRoleId) return const result = await assignAreaLead({ area_lead_role_id: areaLeadRoleId, member_id: memberId }) if (result.error) { toast.error(result.error); return } toast.success('Assigned as area lead') router.refresh() } const [activeTab, setActiveTab] = useState<'all' | 'payment_issues' | 'unverified' | 'inactive'>('all') const [search, setSearch] = useState('') const [tierFilter, setTierFilter] = useState('') const [showAdd, setShowAdd] = useState(false) const [editMember, setEditMember] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') // Add/Edit form const [form, setForm] = useState({ display_name: '', email: '', phone: '', handle: '', tier: 'basic', role: 'member', joined_at: '', has_card_access: false, }) const total = members.length const paymentIssues = members.filter(m => m.status === 'current' && m.payment_status && m.payment_status !== 'current').length const unverified = members.filter(m => m.status === 'unverified').length const filteredMembers = members.filter(m => { const matchesTab = activeTab === 'all' ? (m.status !== 'inactive') : activeTab === 'payment_issues' ? (m.payment_status && m.payment_status !== 'current') : activeTab === 'unverified' ? m.status === 'unverified' : m.status === 'inactive' const matchesSearch = !search || m.display_name.toLowerCase().includes(search.toLowerCase()) || m.email.toLowerCase().includes(search.toLowerCase()) || (m.handle?.toLowerCase().includes(search.toLowerCase()) ?? false) const matchesTier = !tierFilter || m.tier === tierFilter return matchesTab && matchesSearch && matchesTier }) async function handleAdd(e: React.FormEvent) { e.preventDefault() setLoading(true) setError('') const result = await addMember(form) if (result.error) { setError(result.error); setLoading(false); return } if (result.data) setMembers(prev => [...prev, result.data as Member]) setShowAdd(false) setForm({ display_name: '', email: '', phone: '', handle: '', tier: 'basic', role: 'member', joined_at: '', has_card_access: false }) setLoading(false) } async function handleEdit(e: React.FormEvent) { e.preventDefault() if (!editMember) return setLoading(true) setError('') const result = await updateMember(editMember.id, form) if (result.error) { setError(result.error); setLoading(false); return } setMembers(prev => prev.map(m => m.id === editMember.id ? { ...m, ...form } : m)) setEditMember(null) setLoading(false) } async function handleApprove(memberId: string) { const result = await approveMember(memberId) if (result?.error) { toast.error(result.error); return } setMembers(prev => prev.map(m => m.id === memberId ? { ...m, status: 'current', approved: true } : m)) } async function handleRemove(memberId: string) { if (!(await confirm({ title: 'Remove member', description: 'This member will be removed from the space.', confirmText: 'Remove', destructive: true }))) return const result = await removeMember(memberId) if (result?.error) { toast.error(result.error); return } setMembers(prev => prev.filter(m => m.id !== memberId)) } function openEdit(m: Member) { setEditMember(m) setForm({ display_name: m.display_name, email: m.email, phone: m.phone ?? '', handle: m.handle ?? '', tier: m.tier, role: m.role, joined_at: m.joined_at ?? '', has_card_access: m.has_card_access ?? false, }) setError('') } const MemberFormFields = () => ( <>
setForm(f => ({ ...f, display_name: 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 transition" />
setForm(f => ({ ...f, email: 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 transition" />
setForm(f => ({ ...f, phone: 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 transition" />
setForm(f => ({ ...f, handle: e.target.value }))} className="w-full bg-background border border-border rounded px-3 py-2 font-mono text-sm text-foreground focus:outline-none focus:border-primary transition" />
setForm(f => ({ ...f, joined_at: 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" />
) return (
Members {total} total
{isAdmin(currentRole) && ( )}
{[ { key: 'all', label: `All ${total}` }, { key: 'payment_issues', label: `Payment Issues ${paymentIssues}` }, { key: 'unverified', label: `Pending Approval ${unverified}` }, { key: 'inactive', label: 'Inactive' }, ].map(({ key, label }) => ( ))}
setSearch(e.target.value)} className="w-full bg-card border border-border rounded pl-9 pr-4 py-2 font-sans text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition" />
{isAdmin(currentRole) && ( )} {filteredMembers.length > 0 ? filteredMembers.map(m => { const initials = (m.display_name || 'U').split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) const hasIssue = m.payment_status && m.payment_status !== 'current' return ( {isAdmin(currentRole) && ( )} ) }) : ( )}
MEMBER TIER JOINED LAST PAYMENT STATUSACTIONS
{initials}

{m.display_name}

{m.email}{m.handle ? ` · @${m.handle}` : ''}{m.payment_note ? ` — ${m.payment_note}` : ''}

{m.tier?.toUpperCase()} {m.joined_at ? new Date(m.joined_at).toLocaleDateString('en-US', { month: 'short', year: 'numeric' }) : '-'} {(m.last_paid_at || m.last_payment_at) ? new Date((m.last_paid_at || m.last_payment_at)!).toLocaleDateString('en-US', { month: 'short', year: 'numeric' }) : '-'} {m.status === 'unverified' ? ( unverified ) : m.payment_status === 'current' ? ( current ) : m.payment_status ? ( {m.payment_status} ) : ( {m.status} )}
{m.status === 'unverified' && ( )} {areaLeadRoles.length > 0 && ( )}
No members match this filter Try a different tab, clear the search, or change the tier filter.
{/* Add Member Modal */} {showAdd && (

Add Member

{error &&

{error}

}
)} {/* Edit Member Modal */} {editMember && (

Edit {editMember.display_name}

{error &&

{error}

}
)}
) }