Changes
This commit is contained in:
188
src/pages/admin/AdminMembers.tsx
Normal file
188
src/pages/admin/AdminMembers.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
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;
|
||||
full_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">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Nama</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Bergabung</TableHead>
|
||||
<TableHead className="text-right">Aksi</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{members.map((member) => (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell>{member.email || '-'}</TableCell>
|
||||
<TableCell>{member.full_name || '-'}</TableCell>
|
||||
<TableCell>
|
||||
{adminIds.has(member.id) ? (
|
||||
<Badge className="bg-primary">Admin</Badge>
|
||||
) : (
|
||||
<Badge className="bg-secondary">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>
|
||||
</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.full_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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user