219 lines
7.9 KiB
TypeScript
219 lines
7.9 KiB
TypeScript
import { useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { AppLayout } from "@/components/AppLayout";
|
|
import { useCart } from "@/contexts/CartContext";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { supabase } from "@/integrations/supabase/client";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { toast } from "@/hooks/use-toast";
|
|
import { formatIDR } from "@/lib/format";
|
|
import { Trash2, CreditCard, Loader2, QrCode } from "lucide-react";
|
|
|
|
// Edge function base URL - configurable via env with sensible default
|
|
const getEdgeFunctionBaseUrl = (): string => {
|
|
return import.meta.env.VITE_SUPABASE_EDGE_URL || "https://lovable.backoffice.biz.id/functions/v1";
|
|
};
|
|
|
|
const PAKASIR_CALLBACK_URL = `${getEdgeFunctionBaseUrl()}/pakasir-webhook`;
|
|
|
|
type CheckoutStep = "cart" | "payment";
|
|
|
|
export default function Checkout() {
|
|
const { items, removeItem, clearCart, total } = useCart();
|
|
const { user } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [step, setStep] = useState<CheckoutStep>("cart");
|
|
|
|
const checkPaymentStatus = async (oid: string) => {
|
|
const { data: order } = await supabase.from("orders").select("payment_status").eq("id", oid).single();
|
|
|
|
if (order?.payment_status === "paid") {
|
|
toast({ title: "Pembayaran berhasil!", description: "Akses produk sudah aktif" });
|
|
navigate(`/orders/${oid}`);
|
|
}
|
|
};
|
|
|
|
const handleCheckout = async () => {
|
|
if (!user) {
|
|
toast({ title: "Login diperlukan", description: "Silakan login untuk melanjutkan pembayaran" });
|
|
navigate("/auth");
|
|
return;
|
|
}
|
|
|
|
if (items.length === 0) {
|
|
toast({
|
|
title: "Keranjang kosong",
|
|
description: "Tambahkan produk ke keranjang terlebih dahulu",
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
// Generate a unique order reference
|
|
const orderRef = `ORD${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substring(2, 6).toUpperCase()}`;
|
|
const amountInRupiah = Math.round(total);
|
|
|
|
// Create order with pending payment status
|
|
const { data: order, error: orderError } = await supabase
|
|
.from("orders")
|
|
.insert({
|
|
user_id: user.id,
|
|
total_amount: amountInRupiah,
|
|
status: "pending",
|
|
payment_provider: "pakasir",
|
|
payment_reference: orderRef,
|
|
payment_status: "pending",
|
|
payment_method: "qris",
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (orderError || !order) {
|
|
throw new Error("Gagal membuat order");
|
|
}
|
|
|
|
// Create order items
|
|
const orderItems = items.map((item) => ({
|
|
order_id: order.id,
|
|
product_id: item.id,
|
|
unit_price: item.sale_price ?? item.price,
|
|
quantity: 1,
|
|
}));
|
|
|
|
const { error: itemsError } = await supabase.from("order_items").insert(orderItems);
|
|
if (itemsError) throw new Error("Gagal menambahkan item order");
|
|
|
|
// Build description from product titles
|
|
const productTitles = items.map(item => item.title).join(", ");
|
|
|
|
// Call edge function to create QRIS payment
|
|
const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-payment', {
|
|
body: {
|
|
order_id: order.id,
|
|
amount: amountInRupiah,
|
|
description: productTitles,
|
|
},
|
|
});
|
|
|
|
if (paymentError) {
|
|
console.error('Payment creation error:', paymentError);
|
|
throw new Error(paymentError.message || 'Gagal membuat pembayaran');
|
|
}
|
|
|
|
// Clear cart and redirect to order detail page to show QR code
|
|
clearCart();
|
|
navigate(`/orders/${order.id}`);
|
|
} catch (error) {
|
|
console.error("Checkout error:", error);
|
|
toast({
|
|
title: "Error",
|
|
description: error instanceof Error ? error.message : "Terjadi kesalahan saat checkout",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const refreshPaymentStatus = async () => {
|
|
// This function is now handled in OrderDetail page
|
|
// Kept for backwards compatibility but no longer used
|
|
toast({ title: "Info", description: "Status pembayaran diupdate otomatis" });
|
|
};
|
|
|
|
return (
|
|
<AppLayout>
|
|
<div className="container mx-auto px-4 py-8">
|
|
<h1 className="text-4xl font-bold mb-8">Checkout</h1>
|
|
|
|
{items.length === 0 ? (
|
|
<Card className="border-2 border-border">
|
|
<CardContent className="py-12 text-center">
|
|
<p className="text-muted-foreground mb-4">Keranjang belanja Anda kosong</p>
|
|
<Button onClick={() => navigate("/products")} variant="outline" className="border-2">
|
|
Lihat Produk
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<div className="lg:col-span-2 space-y-4">
|
|
{items.map((item) => (
|
|
<Card key={item.id} className="border-2 border-border">
|
|
<CardContent className="py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="font-semibold">{item.title}</h3>
|
|
<p className="text-sm text-muted-foreground capitalize">{item.type}</p>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="font-bold">{formatIDR(item.sale_price ?? item.price)}</span>
|
|
<Button variant="ghost" size="sm" onClick={() => removeItem(item.id)}>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
|
|
<Card className="border-2 border-border">
|
|
<CardHeader>
|
|
<CardTitle>Metode Pembayaran</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center space-x-3 p-3 border-2 border-border rounded-none bg-muted">
|
|
<QrCode className="w-5 h-5" />
|
|
<div>
|
|
<p className="font-medium">QRIS</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Scan QR dengan aplikasi e-wallet atau mobile banking
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div>
|
|
<Card className="border-2 border-border shadow-md sticky top-4">
|
|
<CardHeader>
|
|
<CardTitle>Ringkasan Order</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex justify-between text-lg">
|
|
<span>Total</span>
|
|
<span className="font-bold">{formatIDR(total)}</span>
|
|
</div>
|
|
<Button onClick={handleCheckout} className="w-full shadow-sm" disabled={loading}>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
Memproses...
|
|
</>
|
|
) : user ? (
|
|
<>
|
|
<CreditCard className="w-4 h-4 mr-2" />
|
|
Bayar dengan QRIS
|
|
</>
|
|
) : (
|
|
"Login untuk Checkout"
|
|
)}
|
|
</Button>
|
|
<p className="text-xs text-muted-foreground text-center">Pembayaran diproses melalui Pakasir</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|