Files
meet-hub/src/pages/admin/AdminMembers.tsx
dwindown 534c9629ea Fix JSX tag mismatches in mobile card layouts
Fixed build errors caused by incomplete sed script replacement.
Changed mismatched closing </CardContent> and </Card> tags to </div>
in mobile card layouts across admin pages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-25 10:40:47 +07:00

249 lines
10 KiB
TypeScript

import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/hooks/useAuth";
import { AppLayout } from "@/components/AppLayout";
import { Card, CardContent } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { formatDateTime } from "@/lib/format";
import { Eye, Shield, ShieldOff } from "lucide-react";
import { toast } from "@/hooks/use-toast";
interface Member {
id: string;
email: string | null;
name: string | null;
created_at: string;
isAdmin?: boolean;
}
interface UserAccess {
id: string;
granted_at: string;
product: { title: string };
}
export default function AdminMembers() {
const { user, isAdmin, loading: authLoading } = useAuth();
const navigate = useNavigate();
const [members, setMembers] = useState<Member[]>([]);
const [adminIds, setAdminIds] = useState<Set<string>>(new Set());
const [loading, setLoading] = useState(true);
const [selectedMember, setSelectedMember] = useState<Member | null>(null);
const [memberAccess, setMemberAccess] = useState<UserAccess[]>([]);
const [dialogOpen, setDialogOpen] = useState(false);
useEffect(() => {
if (!authLoading) {
if (!user) navigate("/auth");
else if (!isAdmin) navigate("/dashboard");
else fetchMembers();
}
}, [user, isAdmin, authLoading]);
const fetchMembers = async () => {
const [profilesRes, rolesRes] = await Promise.all([
supabase.from("profiles").select("*").order("created_at", { ascending: false }),
supabase.from("user_roles").select("user_id").eq("role", "admin"),
]);
const admins = new Set((rolesRes.data || []).map((r) => r.user_id));
setAdminIds(admins);
if (profilesRes.data) {
setMembers(profilesRes.data.map((p) => ({ ...p, isAdmin: admins.has(p.id) })));
}
setLoading(false);
};
const viewMemberDetails = async (member: Member) => {
setSelectedMember(member);
const { data } = await supabase.from("user_access").select("*, product:products(title)").eq("user_id", member.id);
setMemberAccess((data as unknown as UserAccess[]) || []);
setDialogOpen(true);
};
const toggleAdminRole = async (memberId: string, currentlyAdmin: boolean) => {
if (currentlyAdmin) {
const { error } = await supabase.from("user_roles").delete().eq("user_id", memberId).eq("role", "admin");
if (error) toast({ title: "Error", description: "Gagal menghapus role admin", variant: "destructive" });
else {
toast({ title: "Berhasil", description: "Role admin dihapus" });
fetchMembers();
}
} else {
const { error } = await supabase.from("user_roles").insert({ user_id: memberId, role: "admin" });
if (error) toast({ title: "Error", description: error.message, variant: "destructive" });
else {
toast({ title: "Berhasil", description: "Role admin ditambahkan" });
fetchMembers();
}
}
};
if (authLoading || loading) {
return (
<AppLayout>
<div className="container mx-auto px-4 py-8">
<Skeleton className="h-10 w-1/3 mb-8" />
<Skeleton className="h-64 w-full" />
</div>
</AppLayout>
);
}
return (
<AppLayout>
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-2">Manajemen Member</h1>
<p className="text-muted-foreground mb-8">Kelola semua pengguna</p>
<Card className="border-2 border-border">
<CardContent className="p-0">
{/* Desktop Table */}
<div className="hidden md:block overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="whitespace-nowrap">Email</TableHead>
<TableHead className="whitespace-nowrap">Nama</TableHead>
<TableHead className="whitespace-nowrap">Role</TableHead>
<TableHead className="whitespace-nowrap">Bergabung</TableHead>
<TableHead className="text-right whitespace-nowrap">Aksi</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{members.map((member) => (
<TableRow key={member.id}>
<TableCell>{member.email || "-"}</TableCell>
<TableCell>{member.name || "-"}</TableCell>
<TableCell>
{adminIds.has(member.id) ? (
<Badge className="bg-primary">Admin</Badge>
) : (
<Badge className="bg-secondary text-primary">Member</Badge>
)}
</TableCell>
<TableCell>{formatDateTime(member.created_at)}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" onClick={() => viewMemberDetails(member)}>
<Eye className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => toggleAdminRole(member.id, adminIds.has(member.id))}
disabled={member.id === user?.id}
>
{adminIds.has(member.id) ? <ShieldOff className="w-4 h-4" /> : <Shield className="w-4 h-4" />}
</Button>
</TableCell>
</TableRow>
))}
{members.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
Belum ada member
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Mobile Card Layout */}
<div className="md:hidden space-y-3 p-4">
{members.map((member) => (
<div key={member.id} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card">
<div>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-base line-clamp-1">{member.name || "Tanpa Nama"}</h3>
<p className="text-sm text-muted-foreground truncate">{member.email || "-"}</p>
</div>
{adminIds.has(member.id) ? (
<Badge className="bg-primary shrink-0">Admin</Badge>
) : (
<Badge className="bg-secondary text-primary shrink-0">Member</Badge>
)}
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Bergabung:</span>
<span className="text-sm">{formatDateTime(member.created_at)}</span>
</div>
</div>
<div className="flex gap-2 pt-2 border-t border-border">
<Button variant="ghost" size="sm" onClick={() => viewMemberDetails(member)} className="flex-1">
<Eye className="w-4 h-4 mr-1" />
Detail
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => toggleAdminRole(member.id, adminIds.has(member.id))}
disabled={member.id === user?.id}
className="flex-1"
>
{adminIds.has(member.id) ? <ShieldOff className="w-4 h-4 mr-1" /> : <Shield className="w-4 h-4 mr-1" />}
{adminIds.has(member.id) ? "Hapus Admin" : "Jadikan Admin"}
</Button>
</div>
</div>
</div>
))}
{members.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Belum ada member
</div>
)}
</div>
</CardContent>
</Card>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-lg border-2 border-border">
<DialogHeader>
<DialogTitle>Detail Member</DialogTitle>
</DialogHeader>
{selectedMember && (
<div className="space-y-4">
<div className="text-sm space-y-1">
<p>
<span className="text-muted-foreground">Email:</span> {selectedMember.email}
</p>
<p>
<span className="text-muted-foreground">Nama:</span> {selectedMember.name || "-"}
</p>
<p>
<span className="text-muted-foreground">ID:</span> {selectedMember.id}
</p>
</div>
<div className="border-t border-border pt-4">
<p className="font-medium mb-2">Akses Produk:</p>
{memberAccess.length === 0 ? (
<p className="text-muted-foreground text-sm">Tidak ada akses</p>
) : (
<div className="space-y-1">
{memberAccess.map((access) => (
<div key={access.id} className="flex justify-between text-sm">
<span>{access.product?.title}</span>
<span className="text-muted-foreground">{formatDateTime(access.granted_at)}</span>
</div>
))}
</div>
)}
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
</AppLayout>
);
}