Files
meet-hub/src/pages/Checkout.tsx
dwindown fb24e77e42 Implement post-implementation refinements
Features implemented:
1. Expired QRIS order handling with dual-path approach
   - Product orders: QR regeneration button
   - Consulting orders: Immediate cancellation with slot release
2. Standardized status badge wording to "Pending"
3. Fixed TypeScript error in MemberDashboard
4. Dynamic badge colors from branding settings
5. Dynamic page title from branding settings
6. Logo/favicon file upload with auto-delete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-24 11:42:20 +07:00

225 lines
8.4 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>
<div className="space-y-3 pt-2 border-t">
<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>
<div className="space-y-1">
<p className="text-xs text-muted-foreground text-center">Pembayaran aman dengan standar QRIS dari Bank Indonesia</p>
<p className="text-xs text-muted-foreground text-center">Diproses oleh mitra pembayaran terpercaya</p>
</div>
<p className="text-xs text-muted-foreground text-center pt-1">Didukung oleh Pakasir | QRIS terdaftar oleh Bank Indonesia</p>
</div>
</CardContent>
</Card>
</div>
</div>
)}
</div>
</AppLayout>
);
}