diff --git a/app/(app)/members/members-client.tsx b/app/(app)/members/members-client.tsx index 2f68eee..dce777f 100644 --- a/app/(app)/members/members-client.tsx +++ b/app/(app)/members/members-client.tsx @@ -4,7 +4,7 @@ import { useState, type ReactNode } from 'react' import { Plus, ChevronDown, Users } from 'lucide-react' import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { addMember, updateMember, approveMember, removeMember, assignAreaLead } from '@/lib/actions' +import { addMember, updateMember, approveMember, bulkApproveMembers, removeMember, assignAreaLead } from '@/lib/actions' import { toast } from 'sonner' import { useRouter } from 'next/navigation' import type { Tables } from '@/types/database' @@ -66,6 +66,24 @@ export function MembersClient({ members: initialMembers, currentRole, areaLeadRo const [search, setSearch] = useState('') const [tierFilter, setTierFilter] = useState('') const [sort, setSort] = useState<{ key: SortKey; dir: 'asc' | 'desc' } | null>(null) + const [selected, setSelected] = useState>(new Set()) + const canBulk = isAdmin(currentRole) + const toggleSel = (id: string) => + setSelected(prev => { + const n = new Set(prev) + n.has(id) ? n.delete(id) : n.add(id) + return n + }) + 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 [showAdd, setShowAdd] = useState(false) const [editMember, setEditMember] = useState(null) const [loading, setLoading] = useState(false) @@ -305,11 +323,37 @@ export function MembersClient({ members: initialMembers, currentRole, areaLeadRo + {canBulk && selected.size > 0 && ( +
+ {selected.size} selected + + + + +
+ )} +
+ {canBulk && ( + + )} {([ ['name', 'MEMBER', ''], ['tier', 'TIER', ''], @@ -348,6 +392,16 @@ export function MembersClient({ members: initialMembers, currentRole, areaLeadRo const hasIssue = m.payment_status && m.payment_status !== 'current' return ( + {canBulk && ( + + )}
+ 0 && sortedMembers.every(m => selected.has(m.id))} + onChange={e => + setSelected(e.target.checked ? new Set(sortedMembers.map(m => m.id)) : new Set()) + } + /> +
+ toggleSel(m.id)} + /> +
-
+ @@ -488,9 +542,20 @@ export function MembersClient({ members: initialMembers, currentRole, areaLeadRo return (
-
-

{m.display_name}

-

{m.email}

+
+ {canBulk && ( + toggleSel(m.id)} + /> + )} +
+

{m.display_name}

+

{m.email}

+
{m.tier?.toUpperCase()} diff --git a/lib/actions/members.ts b/lib/actions/members.ts index ebc9425..da27289 100644 --- a/lib/actions/members.ts +++ b/lib/actions/members.ts @@ -17,6 +17,7 @@ import { updateMyProfileSchema, discloseAffiliationsSchema, uuidSchema, + bulkMemberIdsSchema, } from '@/lib/validations' export async function addMember(formData: { @@ -131,6 +132,30 @@ export async function approveMember(memberId: string) { return { success: true as const } } +export async function bulkApproveMembers(ids: unknown) { + const v = parseInput(bulkMemberIdsSchema, ids) + if (!v.ok) return { error: v.error } + + const supabase = await createClient() + const auth = await requireMemberWithRole(supabase, ADMIN_ROLES, 'Admin access required') + if (!auth.ok) return { error: auth.error } + const { member: self } = auth + + // One batched, space-scoped update; only still-unverified rows flip. + const { data, error } = await supabase + .from('space_members') + .update({ status: 'current', approved: true }) + .in('id', v.data) + .eq('space_id', self.space_id) + .eq('status', 'unverified') + .select('id') + if (error) return { error: error.message } + + await logActivity(supabase, self, 'approved', 'member', null, `${data?.length ?? 0} member(s) bulk-approved`) + revalidatePath('/members') + return { data: { approved: data?.length ?? 0 } } +} + /** * Self-edit profile fields: display_name, handle, phone, skills, interests, * willing_to. The privilege-escalation trigger (migration 015) blocks diff --git a/lib/validations.ts b/lib/validations.ts index 817631c..9f86eb6 100644 --- a/lib/validations.ts +++ b/lib/validations.ts @@ -1001,6 +1001,11 @@ export const checkOutSchema = z.object({ export const uuidSchema = z.string().uuid('Invalid ID format') +export const bulkMemberIdsSchema = z + .array(z.string().uuid('Invalid member ID')) + .min(1, 'Select at least one member') + .max(1000) + // Helper type exports export type CreateTaskInput = z.infer export type CreateProjectInput = z.infer