144 lines
5.6 KiB
TypeScript
144 lines
5.6 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, 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} text-primary`} />
|
|
<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>
|
|
);
|
|
}
|