242 lines
9.3 KiB
TypeScript
242 lines
9.3 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { AppLayout } from "@/components/AppLayout";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { supabase } from "@/integrations/supabase/client";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { formatIDR } from "@/lib/format";
|
|
import { Video, Calendar, BookOpen, ArrowRight, Package, Receipt, ShoppingBag } from "lucide-react";
|
|
import { WhatsAppBanner } from "@/components/WhatsAppBanner";
|
|
|
|
interface UserAccess {
|
|
id: string;
|
|
product: {
|
|
id: string;
|
|
title: string;
|
|
slug: string;
|
|
type: string;
|
|
meeting_link: string | null;
|
|
recording_url: string | null;
|
|
};
|
|
}
|
|
|
|
interface Order {
|
|
id: string;
|
|
total_amount: number;
|
|
payment_status: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export default function MemberDashboard() {
|
|
const { user, loading: authLoading } = useAuth();
|
|
const navigate = useNavigate();
|
|
const [access, setAccess] = useState<UserAccess[]>([]);
|
|
const [recentOrders, setRecentOrders] = useState<Order[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [hasWhatsApp, setHasWhatsApp] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!authLoading && !user) navigate("/auth");
|
|
else if (user) fetchData();
|
|
}, [user, authLoading]);
|
|
|
|
const fetchData = async () => {
|
|
const [accessRes, ordersRes, paidOrdersRes, profileRes] = await Promise.all([
|
|
supabase
|
|
.from("user_access")
|
|
.select(`id, product:products (id, title, slug, type, meeting_link, recording_url)`)
|
|
.eq("user_id", user!.id),
|
|
supabase.from("orders").select("*").eq("user_id", user!.id).order("created_at", { ascending: false }).limit(3),
|
|
// Also get products from paid orders (via order_items)
|
|
supabase
|
|
.from("orders")
|
|
.select(
|
|
`
|
|
order_items (
|
|
product:products (id, title, slug, type, meeting_link, recording_url)
|
|
)
|
|
`,
|
|
)
|
|
.eq("user_id", user!.id)
|
|
.eq("payment_status", "paid")
|
|
.eq("payment_provider", "pakasir"),
|
|
supabase.from("profiles").select("whatsapp_number").eq("id", user!.id).single(),
|
|
]);
|
|
|
|
// Combine access from user_access and paid orders
|
|
const directAccess = (accessRes.data as unknown as UserAccess[]) || [];
|
|
const paidProductAccess: UserAccess[] = [];
|
|
|
|
if (paidOrdersRes.data) {
|
|
const existingIds = new Set(directAccess.map((a) => a.product.id));
|
|
paidOrdersRes.data.forEach((order: any) => {
|
|
order.order_items?.forEach((item: any) => {
|
|
if (item.product && !existingIds.has(item.product.id)) {
|
|
existingIds.add(item.product.id);
|
|
paidProductAccess.push({ id: `paid-${item.product.id}`, product: item.product });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
setAccess([...directAccess, ...paidProductAccess]);
|
|
if (ordersRes.data) setRecentOrders(ordersRes.data);
|
|
if (profileRes.data) setHasWhatsApp(!!profileRes.data.whatsapp_number);
|
|
setLoading(false);
|
|
};
|
|
|
|
const getQuickAction = (item: UserAccess) => {
|
|
switch (item.product.type) {
|
|
case "consulting":
|
|
return { label: "Jadwalkan", icon: Calendar, href: item.product.meeting_link };
|
|
case "webinar":
|
|
return { label: "Tonton", icon: Video, href: item.product.recording_url || item.product.meeting_link };
|
|
case "bootcamp":
|
|
return { label: "Lanjutkan", icon: BookOpen, route: `/bootcamp/${item.product.slug}` };
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
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 gap-4 md:grid-cols-2">
|
|
{[...Array(4)].map((_, i) => (
|
|
<Skeleton key={i} className="h-32" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AppLayout>
|
|
<div className="container mx-auto px-4 py-8">
|
|
<h1 className="text-4xl font-bold mb-2">Dashboard</h1>
|
|
<p className="text-muted-foreground mb-8">Selamat datang kembali!</p>
|
|
|
|
{!hasWhatsApp && <WhatsAppBanner />}
|
|
|
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 mb-8">
|
|
<Card className="border-2 border-border">
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center gap-4">
|
|
<Package className="w-10 h-10 text-primary" />
|
|
<div>
|
|
<p className="text-3xl font-bold">{access.length}</p>
|
|
<p className="text-muted-foreground">Produk Diakses</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="border-2 border-border">
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center gap-4">
|
|
<Receipt className="w-10 h-10 text-primary" />
|
|
<div>
|
|
<p className="text-3xl font-bold">{recentOrders.length}</p>
|
|
<p className="text-muted-foreground">Order Terbaru</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="border-2 border-border col-span-full lg:col-span-1">
|
|
<CardContent className="pt-6 flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<ShoppingBag className="w-10 h-10 text-primary" />
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Jelajahi lebih banyak</p>
|
|
<p className="font-medium">Lihat semua produk</p>
|
|
</div>
|
|
</div>
|
|
<Button variant="outline" onClick={() => navigate("/products")} className="border-2">
|
|
<ArrowRight className="w-4 h-4" />
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{access.length > 0 && (
|
|
<div className="mb-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-2xl font-bold">Akses Cepat</h2>
|
|
<Button variant="ghost" onClick={() => navigate("/access")}>
|
|
Lihat Semua
|
|
<ArrowRight className="w-4 h-4 ml-2" />
|
|
</Button>
|
|
</div>
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{access.slice(0, 3).map((item) => {
|
|
const action = getQuickAction(item);
|
|
return (
|
|
<Card key={item.id} className="border-2 border-border">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-lg">{item.product.title}</CardTitle>
|
|
<CardDescription className="capitalize">{item.product.type}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{action &&
|
|
(action.route ? (
|
|
<Button onClick={() => navigate(action.route!)} className="w-full shadow-sm">
|
|
<action.icon className="w-4 h-4 mr-2" />
|
|
{action.label}
|
|
</Button>
|
|
) : action.href ? (
|
|
<Button asChild variant="outline" className="w-full border-2">
|
|
<a href={action.href} target="_blank" rel="noopener noreferrer">
|
|
<action.icon className="w-4 h-4 mr-2" />
|
|
{action.label}
|
|
</a>
|
|
</Button>
|
|
) : null)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{recentOrders.length > 0 && (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-2xl font-bold">Order Terbaru</h2>
|
|
<Button variant="ghost" onClick={() => navigate("/orders")}>
|
|
Lihat Semua
|
|
<ArrowRight className="w-4 h-4 ml-2" />
|
|
</Button>
|
|
</div>
|
|
<Card className="border-2 border-border">
|
|
<CardContent className="p-0 divide-y divide-border">
|
|
{recentOrders.map((order) => (
|
|
<div key={order.id} className="flex items-center justify-between p-4">
|
|
<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="flex items-center gap-4">
|
|
<Badge className={order.payment_status === "paid" ? "bg-accent" : "bg-muted"}>
|
|
{order.payment_status === "paid" ? "Lunas" : "Pending"}
|
|
</Badge>
|
|
<span className="font-bold">{formatIDR(order.total_amount)}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|