diff --git a/src/pages/member/OrderDetail.tsx b/src/pages/member/OrderDetail.tsx index 47233fc..cf244ba 100644 --- a/src/pages/member/OrderDetail.tsx +++ b/src/pages/member/OrderDetail.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { useNavigate, useParams, Link } from "react-router-dom"; import { AppLayout } from "@/components/AppLayout"; import { useAuth } from "@/hooks/useAuth"; @@ -8,8 +8,10 @@ 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 { Alert, AlertDescription } from "@/components/ui/alert"; import { formatIDR, formatDate } from "@/lib/format"; -import { ArrowLeft, Package, CreditCard, Calendar, AlertCircle, Video } from "lucide-react"; +import { ArrowLeft, Package, CreditCard, Calendar, AlertCircle, Video, Clock, RefreshCw } from "lucide-react"; +import { QRCodeSVG } from "qrcode.react"; interface OrderItem { id: string; @@ -32,6 +34,8 @@ interface Order { payment_method: string | null; payment_provider: string | null; payment_url: string | null; + qr_string: string | null; + qr_expires_at: string | null; created_at: string; updated_at: string; order_items: OrderItem[]; @@ -54,26 +58,13 @@ export default function OrderDetail() { const [consultingSlots, setConsultingSlots] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [timeRemaining, setTimeRemaining] = useState(""); + const [isPolling, setIsPolling] = useState(false); - useEffect(() => { - if (authLoading) return; - - if (!user) { - navigate("/auth"); - return; - } - - if (id) { - fetchOrder(); - } - }, [user, authLoading, id]); - - const fetchOrder = async () => { + // Memoized fetchOrder to avoid recreating on every render + const fetchOrder = useCallback(async () => { if (!user || !id) return; - setLoading(true); - setError(null); - try { const { data, error: queryError } = await supabase .from("orders") @@ -92,42 +83,114 @@ export default function OrderDetail() { if (queryError) { console.error("Order fetch error:", queryError); - setError("Gagal mengambil data order"); - setLoading(false); - return; + return null; } - if (!data) { - setError("Order tidak ditemukan"); - setLoading(false); - return; - } - - setOrder(data as Order); - - // Fetch consulting slots if this is a consulting order - const hasConsultingProduct = data.order_items.some( - (item: OrderItem) => item.products.type === "consulting" - ); - - if (hasConsultingProduct) { - const { data: slots } = await supabase - .from("consulting_slots") - .select("*") - .eq("order_id", id) - .order("date", { ascending: true }); - - if (slots) { - setConsultingSlots(slots as ConsultingSlot[]); - } - } + return data as Order; } catch (err) { console.error("Unexpected error:", err); - setError("Terjadi kesalahan"); - } finally { - setLoading(false); + return null; } - }; + }, [user, id]); + + useEffect(() => { + if (authLoading) return; + + if (!user) { + navigate("/auth"); + return; + } + + if (id) { + const loadOrder = async () => { + setLoading(true); + const data = await fetchOrder(); + + if (!data) { + setError("Order tidak ditemukan"); + } else { + setOrder(data); + + // Fetch consulting slots if this is a consulting order + const hasConsultingProduct = data.order_items.some( + (item: OrderItem) => item.products.type === "consulting" + ); + + if (hasConsultingProduct) { + const { data: slots } = await supabase + .from("consulting_slots") + .select("*") + .eq("order_id", id) + .order("date", { ascending: true }); + + if (slots) { + setConsultingSlots(slots as ConsultingSlot[]); + } + } + } + + setLoading(false); + }; + + loadOrder(); + } + }, [user, authLoading, id, fetchOrder]); + + // Poll for payment status if order is pending and has QR string + useEffect(() => { + if (!order || order.payment_status === "paid") return; + + // Only poll if there's a QR string or it's a pending QRIS payment + const shouldPoll = order.qr_string || (order.payment_status === "pending" && order.payment_method === "qris"); + + if (!shouldPoll) return; + + setIsPolling(true); + + const interval = setInterval(async () => { + const updatedOrder = await fetchOrder(); + + if (updatedOrder) { + setOrder(updatedOrder); + + // Stop polling if paid + if (updatedOrder.payment_status === "paid") { + clearInterval(interval); + setIsPolling(false); + } + } + }, 10000); // Poll every 10 seconds + + return () => { + clearInterval(interval); + setIsPolling(false); + }; + }, [order, fetchOrder]); + + // Countdown timer for QR expiration + useEffect(() => { + if (!order?.qr_expires_at) return; + + const updateCountdown = () => { + const now = new Date().getTime(); + const expiresAt = new Date(order.qr_expires_at!).getTime(); + const distance = expiresAt - now; + + if (distance < 0) { + setTimeRemaining("QR Code kadaluarsa"); + return; + } + + const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((distance % (1000 * 60)) / 1000); + setTimeRemaining(`${minutes}m ${seconds}s`); + }; + + updateCountdown(); + const timer = setInterval(updateCountdown, 1000); + + return () => clearInterval(timer); + }, [order?.qr_expires_at]); const getStatusColor = (status: string) => { switch (status) { @@ -265,7 +328,54 @@ export default function OrderDetail() { )} - {order.payment_status === "pending" && order.payment_url && ( + {/* QR Code Display for pending QRIS payments */} + {order.payment_status === "pending" && order.payment_method === "qris" && order.qr_string && ( +
+ + + + Scan QR code ini dengan aplikasi e-wallet atau mobile banking Anda + {timeRemaining && ( + + (Kadaluarsa dalam {timeRemaining}) + + )} + + + +
+
+ +
+ +
+

{formatIDR(order.total_amount)}

+

+ Order ID: {order.id.slice(0, 8)} +

+
+ + {isPolling && ( +
+ + Menunggu pembayaran... +
+ )} + + {order.payment_url && ( + + )} +
+
+ )} + + {/* Fallback button for pending payments without QR */} + {order.payment_status === "pending" && !order.qr_string && order.payment_url && (