This commit is contained in:
gpt-engineer-app[bot]
2025-12-19 15:17:47 +00:00
parent f57bba6f9c
commit 7fc10126df
11 changed files with 979 additions and 88 deletions

View File

@@ -0,0 +1,258 @@
import { useEffect, useState } from "react";
import { useNavigate, useParams, Link } from "react-router-dom";
import { AppLayout } from "@/components/AppLayout";
import { useAuth } from "@/hooks/useAuth";
import { supabase } from "@/integrations/supabase/client";
import { Card, CardContent, 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 { Separator } from "@/components/ui/separator";
import { formatIDR, formatDate } from "@/lib/format";
import { ArrowLeft, Package, CreditCard, Calendar, Clock } from "lucide-react";
interface OrderItem {
id: string;
product_id: string;
quantity: number;
price: number;
products: {
title: string;
type: string;
slug: string;
};
}
interface Order {
id: string;
total_amount: number;
status: string;
payment_status: string | null;
payment_method: string | null;
payment_provider: string | null;
payment_url: string | null;
created_at: string;
updated_at: string;
order_items: OrderItem[];
}
export default function OrderDetail() {
const { id } = useParams<{ id: string }>();
const { user, loading: authLoading } = useAuth();
const navigate = useNavigate();
const [order, setOrder] = useState<Order | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!authLoading && !user) navigate("/auth");
else if (user && id) fetchOrder();
}, [user, authLoading, id]);
const fetchOrder = async () => {
const { data, error } = await supabase
.from("orders")
.select(`
*,
order_items (
id,
product_id,
quantity,
price,
products (title, type, slug)
)
`)
.eq("id", id)
.eq("user_id", user!.id)
.single();
if (error || !data) {
navigate("/orders");
return;
}
setOrder(data as Order);
setLoading(false);
};
const getStatusColor = (status: string) => {
switch (status) {
case "paid":
return "bg-accent text-primary";
case "pending":
return "bg-secondary text-primary";
case "cancelled":
case "failed":
return "bg-destructive";
default:
return "bg-secondary text-primary";
}
};
const getStatusLabel = (status: string | null) => {
switch (status) {
case "paid":
return "Lunas";
case "pending":
return "Menunggu Pembayaran";
case "failed":
return "Gagal";
case "cancelled":
return "Dibatalkan";
default:
return status || "Pending";
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case "consulting":
return "Konsultasi";
case "webinar":
return "Webinar";
case "bootcamp":
return "Bootcamp";
default:
return type;
}
};
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>
);
}
if (!order) return null;
return (
<AppLayout>
<div className="container mx-auto px-4 py-8 max-w-3xl">
<Button
variant="ghost"
onClick={() => navigate("/orders")}
className="mb-4"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Kembali ke Riwayat Order
</Button>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold">Detail Order</h1>
<p className="text-muted-foreground font-mono">#{order.id.slice(0, 8)}</p>
</div>
<Badge className={getStatusColor(order.payment_status || order.status)}>
{getStatusLabel(order.payment_status || order.status)}
</Badge>
</div>
{/* Order Info */}
<Card className="border-2 border-border mb-6">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Calendar className="w-5 h-5" />
Informasi Order
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-muted-foreground">Tanggal Order</p>
<p className="font-medium">{formatDate(order.created_at)}</p>
</div>
<div>
<p className="text-muted-foreground">Terakhir Update</p>
<p className="font-medium">{formatDate(order.updated_at)}</p>
</div>
{order.payment_method && (
<div>
<p className="text-muted-foreground">Metode Pembayaran</p>
<p className="font-medium uppercase">{order.payment_method}</p>
</div>
)}
{order.payment_provider && (
<div>
<p className="text-muted-foreground">Provider</p>
<p className="font-medium capitalize">{order.payment_provider}</p>
</div>
)}
</div>
{order.payment_status === "pending" && order.payment_url && (
<div className="pt-4">
<Button asChild className="w-full shadow-sm">
<a href={order.payment_url} target="_blank" rel="noopener noreferrer">
<CreditCard className="w-4 h-4 mr-2" />
Lanjutkan Pembayaran
</a>
</Button>
</div>
)}
</CardContent>
</Card>
{/* Order Items */}
<Card className="border-2 border-border mb-6">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Package className="w-5 h-5" />
Item Pesanan
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{order.order_items.map((item) => (
<div key={item.id} className="flex items-center justify-between py-2">
<div className="flex-1">
<Link
to={`/products/${item.products.slug}`}
className="font-medium hover:underline"
>
{item.products.title}
</Link>
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="text-xs">
{getTypeLabel(item.products.type)}
</Badge>
<span className="text-sm text-muted-foreground">
x{item.quantity}
</span>
</div>
</div>
<p className="font-medium">{formatIDR(item.price)}</p>
</div>
))}
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between text-lg font-bold">
<span>Total</span>
<span>{formatIDR(order.total_amount)}</span>
</div>
</CardContent>
</Card>
{/* Access Info */}
{order.payment_status === "paid" && (
<Card className="border-2 border-primary bg-primary/5">
<CardContent className="py-4">
<p className="text-sm">
Pembayaran berhasil! Akses produk Anda tersedia di halaman{" "}
<Link to="/access" className="font-medium underline">
Akses Saya
</Link>
.
</p>
</CardContent>
</Card>
)}
</div>
</AppLayout>
);
}