'use client' import { useState } from 'react' import { Plus } from 'lucide-react' import { addMember, updateMember, approveMember, bulkApproveMembers, removeMember, assignAreaLead } from '@/lib/actions' import { toast } from 'sonner' import { useRouter } from 'next/navigation' import { PageTitle } from '@/components/ui/page-title' import { useConfirm } from '@/components/ui/confirm' import { MemberCertificationsDialog } from '@/components/certifications/member-certifications-dialog' import { MemberCardsDialog } from '@/components/door/member-cards-dialog' import { MemberFormsDialog } from '@/components/forms/member-forms-dialog' import { MembersTabs, MembersToolbar } from './components/members-toolbar' import { MembersTable } from './components/members-table' import { MemberFormDialog } from './components/member-form-dialog' import type { Member, MemberForm, MemberTab, MembersClientProps } from './types' const isAdmin = (role: string) => role === 'admin' || role === 'board' export function MembersClient({ members: initialMembers, currentRole, areaLeadRoles = [], inviteSlot, canGrantCerts = false, canManageCards = false, canViewForms = false }: MembersClientProps) { const router = useRouter() const confirm = useConfirm() const admin = isAdmin(currentRole) const [members, setMembers] = useState(initialMembers) const [certMember, setCertMember] = useState(null) const [cardMember, setCardMember] = useState(null) const [formsMember, setFormsMember] = useState(null) const [activeTab, setActiveTab] = useState('all') const [search, setSearch] = useState('') const [tierFilter, setTierFilter] = useState('') const [selected, setSelected] = useState>(new Set()) 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 toggleSel = (id: string) => setSelected(prev => { const n = new Set(prev) n.has(id) ? n.delete(id) : n.add(id) return n }) 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() } async function bulkApprove() { const ids = [...selected] if (ids.length === 0) return const res = await bulkApproveMembers(ids) if ('error' in res && res.error) { toast.error(res.error); return } const n = (res as { data: { approved: number } }).data.approved setMembers(prev => prev.map(m => (selected.has(m.id) && m.status === 'unverified' ? { ...m, status: 'current', approved: true } : m))) setSelected(new Set()) toast.success(n > 0 ? `Approved ${n} member(s)` : 'No unverified members in the selection') } 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' // display_name and email are both nullable (e.g. imported members), so // guard before .toLowerCase() — an unguarded access crashed the whole list // the moment an admin typed into the search box. const q = search.toLowerCase() const matchesSearch = !search || (m.display_name?.toLowerCase().includes(q) ?? false) || (m.email?.toLowerCase().includes(q) ?? false) || (m.handle?.toLowerCase().includes(q) ?? 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 } as Member : 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('') } return (
Members {total} total
{admin && ( )}
{inviteSlot &&
{inviteSlot}
} setSelected(new Set())} />
{/* Add Member Modal */} setShowAdd(false)} /> {/* Edit Member Modal */} setEditMember(null)} /> {canGrantCerts && ( setCertMember(null)} /> )} {canManageCards && ( setCardMember(null)} /> )} {canViewForms && ( setFormsMember(null)} /> )}
) }