From 2731d95ae734e4695c081154131a669af1480f10 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 14 Oct 2025 02:21:18 +0000 Subject: [PATCH] Add comprehensive member management component cgen-bbb1cb2e276b4b7f9c777182c1a0fd94 --- .../components/admin/AdminMemberManager.tsx | 632 ++++++++++++++++++ 1 file changed, 632 insertions(+) create mode 100644 client/components/admin/AdminMemberManager.tsx diff --git a/client/components/admin/AdminMemberManager.tsx b/client/components/admin/AdminMemberManager.tsx new file mode 100644 index 00000000..361ed348 --- /dev/null +++ b/client/components/admin/AdminMemberManager.tsx @@ -0,0 +1,632 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Textarea } from "@/components/ui/textarea"; +import { + aethexRoleService, + aethexUserService, + type AethexUserProfile, +} from "@/lib/aethex-database-adapter"; +import { aethexToast } from "@/lib/aethex-toast"; +import { cn } from "@/lib/utils"; +import { + BadgeCheck, + Loader2, + Plus, + RefreshCw, + ShieldCheck, + UserCog, +} from "lucide-react"; + +const roleOptions = [ + "owner", + "admin", + "founder", + "moderator", + "creator", + "mentor", + "staff", + "member", +]; + +const experienceOptions = ["beginner", "intermediate", "advanced", "expert"]; +const userTypeOptions = [ + "game_developer", + "client", + "community_member", + "customer", +]; + +interface AdminMemberManagerProps { + profiles: AethexUserProfile[]; + selectedId: string | null; + onSelectedIdChange: (id: string) => void; + onRefresh: () => Promise; + ownerEmail: string; +} + +interface ProfileDraft { + full_name: string; + location: string; + bio: string; + experience_level: string; + user_type: string; + level: string; + total_xp: string; + loyalty_points: string; +} + +const buildProfileDraft = (profile: AethexUserProfile): ProfileDraft => ({ + full_name: profile.full_name ?? "", + location: profile.location ?? "", + bio: profile.bio ?? "", + experience_level: profile.experience_level ?? "beginner", + user_type: profile.user_type ?? "game_developer", + level: profile.level != null ? String(profile.level) : "", + total_xp: profile.total_xp != null ? String(profile.total_xp) : "", + loyalty_points: + (profile as any).loyalty_points != null + ? String((profile as any).loyalty_points) + : "0", +}); + +const ensureOwnerRoles = (roles: string[], profile: AethexUserProfile | null, ownerEmail: string) => { + if (!profile) return roles; + if ((profile.email ?? "").toLowerCase() !== ownerEmail.toLowerCase()) { + return roles; + } + const required = new Set(["owner", "admin", "founder", ...roles]); + return Array.from(required); +}; + +const normalizeRoles = (roles: string[]): string[] => { + const normalized = roles + .map((role) => role.trim().toLowerCase()) + .filter((role) => role.length > 0); + const unique = Array.from(new Set(normalized)); + return unique.length ? unique : ["member"]; +}; + +const AdminMemberManager = ({ + profiles, + selectedId, + onSelectedIdChange, + onRefresh, + ownerEmail, +}: AdminMemberManagerProps) => { + const [roles, setRoles] = useState([]); + const [loadingRoles, setLoadingRoles] = useState(false); + const [savingRoles, setSavingRoles] = useState(false); + const [savingProfile, setSavingProfile] = useState(false); + const [customRole, setCustomRole] = useState(""); + const [query, setQuery] = useState(""); + const [profileDraft, setProfileDraft] = useState(null); + + const selectedProfile = useMemo( + () => profiles.find((profile) => profile.id === selectedId) ?? null, + [profiles, selectedId], + ); + + useEffect(() => { + if (!selectedId && profiles.length) { + onSelectedIdChange(profiles[0].id); + } + }, [profiles, selectedId, onSelectedIdChange]); + + const loadRoles = useCallback( + async (id: string) => { + setLoadingRoles(true); + try { + const fetched = await aethexRoleService.getUserRoles(id); + setRoles(normalizeRoles(fetched)); + } catch (error) { + console.warn("Failed to load user roles", error); + setRoles(["member"]); + } finally { + setLoadingRoles(false); + } + }, + [], + ); + + useEffect(() => { + if (selectedProfile) { + setProfileDraft(buildProfileDraft(selectedProfile)); + loadRoles(selectedProfile.id).catch(() => undefined); + } else { + setProfileDraft(null); + setRoles([]); + } + }, [selectedProfile, loadRoles]); + + const filteredProfiles = useMemo(() => { + const value = query.trim().toLowerCase(); + if (!value) return profiles; + return profiles.filter((profile) => { + const haystack = [ + profile.full_name, + profile.username, + profile.email, + profile.bio, + profile.location, + profile.role, + ] + .filter(Boolean) + .join(" ") + .toLowerCase(); + return haystack.includes(value); + }); + }, [profiles, query]); + + const handleRoleToggle = (role: string) => { + setRoles((prev) => + prev.includes(role.toLowerCase()) + ? prev.filter((item) => item !== role.toLowerCase()) + : [...prev, role.toLowerCase()], + ); + }; + + const addCustomRole = () => { + const value = customRole.trim().toLowerCase(); + if (!value) return; + setRoles((prev) => (prev.includes(value) ? prev : [...prev, value])); + setCustomRole(""); + }; + + const saveRoles = async () => { + if (!selectedProfile) return; + setSavingRoles(true); + try { + const enforced = ensureOwnerRoles( + normalizeRoles(roles), + selectedProfile, + ownerEmail, + ); + await aethexRoleService.setUserRoles(selectedProfile.id, enforced); + setRoles(enforced); + aethexToast.success({ + title: "Roles updated", + description: `${selectedProfile.full_name ?? selectedProfile.email ?? "Member"} now has ${enforced.join(", ")}.`, + }); + } catch (error: any) { + console.error("Failed to set user roles", error); + aethexToast.error({ + title: "Role update failed", + description: error?.message || "Unable to update roles. Check Supabase policies.", + }); + } finally { + setSavingRoles(false); + } + }; + + const saveProfile = async () => { + if (!selectedProfile || !profileDraft) return; + setSavingProfile(true); + try { + const updates: Partial = { + full_name: profileDraft.full_name.trim() || null, + location: profileDraft.location.trim() || null, + bio: profileDraft.bio.trim() || null, + experience_level: profileDraft.experience_level as any, + user_type: profileDraft.user_type as any, + }; + if (profileDraft.level.trim().length) { + updates.level = Number(profileDraft.level) || 0; + } + if (profileDraft.total_xp.trim().length) { + updates.total_xp = Number(profileDraft.total_xp) || 0; + } + if (profileDraft.loyalty_points.trim().length) { + (updates as any).loyalty_points = Number(profileDraft.loyalty_points) || 0; + } + await aethexUserService.updateProfile(selectedProfile.id, updates); + aethexToast.success({ + title: "Profile updated", + description: `${selectedProfile.full_name ?? selectedProfile.email ?? "Member"} profile saved.`, + }); + await onRefresh(); + } catch (error: any) { + console.error("Failed to update profile", error); + aethexToast.error({ + title: "Profile update failed", + description: error?.message || "Supabase rejected the update. Review payload and RLS policies.", + }); + } finally { + setSavingProfile(false); + } + }; + + const resetDraft = () => { + if (!selectedProfile) return; + setProfileDraft(buildProfileDraft(selectedProfile)); + }; + + return ( +
+ + +
+
+ Directory + Search and select members to administer. +
+ +
+ setQuery(event.target.value)} + className="bg-background/60" + /> +
+ + + + + + Name + Email + Role + + + + {filteredProfiles.map((profile) => { + const active = profile.id === selectedId; + return ( + onSelectedIdChange(profile.id)} + > + +
+ {profile.full_name || profile.username || "Unknown"} + + {profile.username} + +
+
+ + {profile.email || "—"} + + + + {(profile.role || roles[0] || "member").toLowerCase()} + + +
+ ); + })} + {!filteredProfiles.length ? ( + + + No members found. + + + ) : null} +
+
+
+
+
+ +
+ + + + + Member controls + + + Update profile data, loyalty, and Supabase attributes. + + + + {selectedProfile && profileDraft ? ( +
+
+
+ + + setProfileDraft((draft) => + draft + ? { ...draft, full_name: event.target.value } + : draft, + ) + } + className="bg-background/60" + /> +
+
+ + + setProfileDraft((draft) => + draft + ? { ...draft, location: event.target.value } + : draft, + ) + } + className="bg-background/60" + /> +
+
+ + +
+
+ + +
+
+ +
+ +