Changes
This commit is contained in:
134
src/pages/admin/AdminDashboard.tsx
Normal file
134
src/pages/admin/AdminDashboard.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
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, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { formatIDR } from '@/lib/format';
|
||||
import { Package, Users, Receipt, TrendingUp, BookOpen, Calendar } from 'lucide-react';
|
||||
|
||||
interface Stats {
|
||||
totalProducts: number;
|
||||
totalMembers: number;
|
||||
totalOrders: number;
|
||||
totalRevenue: number;
|
||||
pendingOrders: number;
|
||||
activeBootcamps: number;
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const { user, isAdmin, loading: authLoading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [recentOrders, setRecentOrders] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading) {
|
||||
if (!user) navigate('/auth');
|
||||
else if (!isAdmin) navigate('/dashboard');
|
||||
else fetchData();
|
||||
}
|
||||
}, [user, isAdmin, authLoading]);
|
||||
|
||||
const fetchData = async () => {
|
||||
const [productsRes, profilesRes, ordersRes, paidOrdersRes, bootcampRes] = await Promise.all([
|
||||
supabase.from('products').select('id', { count: 'exact' }),
|
||||
supabase.from('profiles').select('id', { count: 'exact' }),
|
||||
supabase.from('orders').select('*').order('created_at', { ascending: false }).limit(5),
|
||||
supabase.from('orders').select('total_amount').eq('payment_status', 'paid'),
|
||||
supabase.from('products').select('id', { count: 'exact' }).eq('type', 'bootcamp').eq('is_active', true),
|
||||
]);
|
||||
|
||||
const pendingRes = await supabase.from('orders').select('id', { count: 'exact' }).eq('payment_status', 'pending');
|
||||
|
||||
const totalRevenue = paidOrdersRes.data?.reduce((sum, o) => sum + (o.total_amount || 0), 0) || 0;
|
||||
|
||||
setStats({
|
||||
totalProducts: productsRes.count || 0,
|
||||
totalMembers: profilesRes.count || 0,
|
||||
totalOrders: ordersRes.data?.length || 0,
|
||||
totalRevenue,
|
||||
pendingOrders: pendingRes.count || 0,
|
||||
activeBootcamps: bootcampRes.count || 0,
|
||||
});
|
||||
|
||||
setRecentOrders(ordersRes.data || []);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (authLoading || loading) {
|
||||
return (
|
||||
<AppLayout>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Skeleton className="h-10 w-1/3 mb-8" />
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[...Array(4)].map((_, i) => <Skeleton key={i} className="h-32" />)}
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const statCards = [
|
||||
{ label: 'Total Produk', value: stats?.totalProducts || 0, icon: Package, color: 'text-primary' },
|
||||
{ label: 'Total Member', value: stats?.totalMembers || 0, icon: Users, color: 'text-accent' },
|
||||
{ label: 'Order Pending', value: stats?.pendingOrders || 0, icon: Receipt, color: 'text-secondary-foreground' },
|
||||
{ label: 'Total Pendapatan', value: formatIDR(stats?.totalRevenue || 0), icon: TrendingUp, color: 'text-primary' },
|
||||
{ label: 'Bootcamp Aktif', value: stats?.activeBootcamps || 0, icon: BookOpen, color: 'text-accent' },
|
||||
];
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-4xl font-bold mb-2">Admin Dashboard</h1>
|
||||
<p className="text-muted-foreground mb-8">Ringkasan statistik platform</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
|
||||
{statCards.map((stat) => (
|
||||
<Card key={stat.label} className="border-2 border-border">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<stat.icon className={`w-8 h-8 ${stat.color}`} />
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{stat.value}</p>
|
||||
<p className="text-xs text-muted-foreground">{stat.label}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card className="border-2 border-border">
|
||||
<CardHeader>
|
||||
<CardTitle>Order Terbaru</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{recentOrders.length === 0 ? (
|
||||
<p className="text-muted-foreground text-center py-8">Belum ada order</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{recentOrders.map((order) => (
|
||||
<div key={order.id} className="flex items-center justify-between py-2 border-b border-border last:border-0">
|
||||
<div>
|
||||
<p className="font-mono text-sm">{order.id.slice(0, 8)}</p>
|
||||
<p className="text-xs text-muted-foreground">{new Date(order.created_at).toLocaleDateString('id-ID')}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-bold">{formatIDR(order.total_amount)}</p>
|
||||
<span className={`text-xs px-2 py-0.5 ${order.payment_status === 'paid' ? 'bg-accent text-accent-foreground' : 'bg-muted text-muted-foreground'}`}>
|
||||
{order.payment_status === 'paid' ? 'Lunas' : 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user