Changes: - Update MemberOrders to use getPaymentStatusLabel and getPaymentStatusColor - Update OrderDetail to use centralized helpers - Remove duplicate getStatusColor and getStatusLabel functions - Dashboard.tsx already using imported helpers Benefits: - DRY principle - single source of truth - Consistent Indonesian labels everywhere - Easy to update status styling in one place 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
660 lines
23 KiB
TypeScript
660 lines
23 KiB
TypeScript
import { useEffect, useState, useCallback } 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 { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { formatIDR, formatDate } from "@/lib/format";
|
|
import { ArrowLeft, Package, CreditCard, Calendar, AlertCircle, Video, Clock, RefreshCw } from "lucide-react";
|
|
import { QRCodeSVG } from "qrcode.react";
|
|
import { getPaymentStatusLabel, getPaymentStatusColor, getProductTypeLabel } from "@/lib/statusHelpers";
|
|
|
|
interface OrderItem {
|
|
id: string;
|
|
product_id: string;
|
|
quantity: number;
|
|
products: {
|
|
title: string;
|
|
type: string;
|
|
slug: string;
|
|
price: number;
|
|
sale_price: number | null;
|
|
};
|
|
}
|
|
|
|
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;
|
|
qr_string: string | null;
|
|
qr_expires_at: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
order_items: OrderItem[];
|
|
}
|
|
|
|
interface ConsultingSlot {
|
|
id: string;
|
|
date: string;
|
|
start_time: string;
|
|
end_time: string;
|
|
status: string;
|
|
meet_link?: string;
|
|
}
|
|
|
|
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 [consultingSlots, setConsultingSlots] = useState<ConsultingSlot[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [timeRemaining, setTimeRemaining] = useState<string>("");
|
|
const [isPolling, setIsPolling] = useState(false);
|
|
const [regeneratingQR, setRegeneratingQR] = useState(false);
|
|
|
|
// Check if QR is expired
|
|
const isQrExpired = order?.qr_expires_at
|
|
? new Date(order.qr_expires_at) < new Date()
|
|
: false;
|
|
|
|
// Check if this is a consulting order
|
|
const isConsultingOrder = order?.order_items?.some(
|
|
(item: OrderItem) => item.products.type === "consulting"
|
|
) || false;
|
|
|
|
// Memoized fetchOrder to avoid recreating on every render
|
|
const fetchOrder = useCallback(async () => {
|
|
if (!user || !id) return;
|
|
|
|
try {
|
|
const { data, error: queryError } = await supabase
|
|
.from("orders")
|
|
.select(`
|
|
*,
|
|
order_items (
|
|
id,
|
|
product_id,
|
|
quantity,
|
|
products (title, type, slug, price, sale_price)
|
|
)
|
|
`)
|
|
.eq("id", id)
|
|
.eq("user_id", user.id)
|
|
.single();
|
|
|
|
if (queryError) {
|
|
console.error("Order fetch error:", queryError);
|
|
return null;
|
|
}
|
|
|
|
return data as Order;
|
|
} catch (err) {
|
|
console.error("Unexpected error:", err);
|
|
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 getTypeLabel = (type: string) => {
|
|
switch (type) {
|
|
case "consulting":
|
|
return "Konsultasi";
|
|
case "webinar":
|
|
return "Webinar";
|
|
case "bootcamp":
|
|
return "Bootcamp";
|
|
default:
|
|
return type;
|
|
}
|
|
};
|
|
|
|
// Handle QR regeneration for expired product orders
|
|
const handleRegenerateQR = async () => {
|
|
if (!order || isConsultingOrder) return;
|
|
|
|
setRegeneratingQR(true);
|
|
try {
|
|
// Call create-payment function with existing order_id
|
|
const { data, error } = await supabase.functions.invoke('create-payment', {
|
|
body: {
|
|
order_id: order.id,
|
|
amount: order.total_amount,
|
|
description: order.order_items.map((item: OrderItem) => item.products.title).join(", "),
|
|
},
|
|
});
|
|
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
|
|
// Refresh order data to get new QR
|
|
const updatedOrder = await fetchOrder();
|
|
if (updatedOrder) {
|
|
setOrder(updatedOrder);
|
|
}
|
|
|
|
// Restart polling
|
|
setIsPolling(true);
|
|
} catch (error) {
|
|
console.error('QR regeneration error:', error);
|
|
setError('Gagal me-regenerate QR code. Silakan coba lagi atau buat order baru.');
|
|
} finally {
|
|
setRegeneratingQR(false);
|
|
}
|
|
};
|
|
|
|
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 (error) {
|
|
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>
|
|
<Card className="border-2 border-destructive">
|
|
<CardContent className="py-8 text-center">
|
|
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-destructive" />
|
|
<h2 className="text-xl font-bold mb-2">Error</h2>
|
|
<p className="text-muted-foreground mb-4">{error}</p>
|
|
<Button onClick={() => navigate("/orders")}>
|
|
Kembali ke Riwayat Order
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</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={`${getPaymentStatusColor(order.payment_status || order.status)} rounded-full`}>
|
|
{getPaymentStatusLabel(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>
|
|
|
|
{/* QR Code Display for pending QRIS payments */}
|
|
{order.payment_status === "pending" && order.payment_method === "qris" && order.qr_string && !isQrExpired && (
|
|
<div className="pt-4">
|
|
<Alert className="mb-4">
|
|
<Clock className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Scan QR code ini dengan aplikasi e-wallet atau mobile banking Anda
|
|
{timeRemaining && (
|
|
<span className="ml-2 font-medium">
|
|
(Kadaluarsa dalam {timeRemaining})
|
|
</span>
|
|
)}
|
|
</AlertDescription>
|
|
</Alert>
|
|
|
|
<div className="bg-white p-6 rounded-lg border-2 border-border flex flex-col items-center justify-center space-y-4">
|
|
<div className="bg-white p-2 rounded">
|
|
<QRCodeSVG value={order.qr_string} size={200} />
|
|
</div>
|
|
|
|
<div className="text-center space-y-2">
|
|
<p className="text-2xl font-bold">{formatIDR(order.total_amount)}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Order ID: {order.id.slice(0, 8)}
|
|
</p>
|
|
</div>
|
|
|
|
{isPolling && (
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
Menunggu pembayaran...
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-center gap-4 text-xs text-muted-foreground">
|
|
<span>🔒 Pembayaran Aman</span>
|
|
<span>⚡ QRIS Standar Bank Indonesia</span>
|
|
</div>
|
|
|
|
{order.payment_url && (
|
|
<Button asChild variant="outline" className="w-full">
|
|
<a href={order.payment_url} target="_blank" rel="noopener noreferrer">
|
|
<CreditCard className="w-4 h-4 mr-2" />
|
|
Bayar di Halaman Pembayaran
|
|
</a>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Expired QR Handling */}
|
|
{order.payment_status === "pending" && order.payment_method === "qris" && isQrExpired && (
|
|
<div className="pt-4">
|
|
<Alert className="mb-4 border-orange-200 bg-orange-50">
|
|
<AlertCircle className="h-4 w-4 text-orange-600" />
|
|
<AlertDescription className="text-orange-900">
|
|
{isConsultingOrder
|
|
? "Waktu pembayaran telah habis. Slot konsultasi telah dilepaskan. Silakan buat booking baru."
|
|
: "QR Code telah kadaluarsa. Anda dapat me-regenerate QR code untuk melanjutkan pembayaran."}
|
|
</AlertDescription>
|
|
</Alert>
|
|
|
|
{isConsultingOrder ? (
|
|
// Consulting order - show booking button
|
|
<div className="text-center space-y-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
Order ini telah dibatalkan secara otomatis karena waktu pembayaran habis.
|
|
</p>
|
|
<Button onClick={() => navigate("/consulting-booking")} className="shadow-sm">
|
|
<Calendar className="w-4 h-4 mr-2" />
|
|
Buat Booking Baru
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
// Product order - show regenerate button
|
|
<div className="space-y-4">
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold">{formatIDR(order.total_amount)}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Order ID: {order.id.slice(0, 8)}
|
|
</p>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleRegenerateQR}
|
|
disabled={regeneratingQR}
|
|
className="w-full shadow-sm"
|
|
>
|
|
{regeneratingQR ? (
|
|
<>
|
|
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
|
Memproses...
|
|
</>
|
|
) : (
|
|
<>
|
|
<RefreshCw className="w-4 h-4 mr-2" />
|
|
Regenerate QR Code
|
|
</>
|
|
)}
|
|
</Button>
|
|
|
|
<Button
|
|
onClick={() => navigate("/products")}
|
|
variant="outline"
|
|
className="w-full"
|
|
>
|
|
<Package className="w-4 h-4 mr-2" />
|
|
Kembali ke Produk
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Fallback button for pending payments without QR */}
|
|
{order.payment_status === "pending" && !order.qr_string && 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>
|
|
|
|
{/* Smart Item/Service Display */}
|
|
{order.order_items.length > 0 ? (
|
|
// === Product Orders ===
|
|
<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.products.sale_price || item.products.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>
|
|
) : consultingSlots.length > 0 ? (
|
|
// === Consulting Orders ===
|
|
<Card className="border-2 border-primary bg-primary/5 mb-6">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<Video className="w-5 h-5" />
|
|
Detail Sesi Konsultasi
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Summary Card */}
|
|
<div className="bg-background p-4 rounded-lg border-2 border-border">
|
|
<div className="grid grid-cols-1 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-muted-foreground">Waktu Konsultasi</p>
|
|
<p className="font-bold text-lg">
|
|
{consultingSlots[0].start_time.substring(0,5)} - {consultingSlots[consultingSlots.length-1].end_time.substring(0,5)}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{consultingSlots.length} blok ({consultingSlots.length * 45} menit)
|
|
</p>
|
|
</div>
|
|
|
|
{consultingSlots[0]?.meet_link && (
|
|
<div>
|
|
<p className="text-muted-foreground text-sm">Google Meet Link</p>
|
|
<a
|
|
href={consultingSlots[0].meet_link}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="font-medium text-primary hover:underline text-sm"
|
|
>
|
|
{consultingSlots[0].meet_link.substring(0, 40)}...
|
|
</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Alert */}
|
|
{order.payment_status === "paid" ? (
|
|
<Alert className="bg-green-50 border-green-200">
|
|
<Video className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Pembayaran berhasil! Silakan bergabung sesuai jadwal di bawah.
|
|
</AlertDescription>
|
|
</Alert>
|
|
) : (
|
|
<Alert className="bg-yellow-50 border-yellow-200">
|
|
<Clock className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Selesaikan pembayaran untuk mengkonfirmasi jadwal sesi konsultasi.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
|
|
{/* Consulting Slots Detail */}
|
|
{consultingSlots.length > 0 && (
|
|
<Card className="border-2 border-primary bg-primary/5">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<Video className="w-5 h-5" />
|
|
Jadwal Konsultasi
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{consultingSlots.map((slot) => (
|
|
<div key={slot.id} className="border-2 border-border rounded-lg p-4 bg-background">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Badge variant={slot.status === "confirmed" ? "default" : "secondary"}>
|
|
{slot.status === "confirmed" ? "Terkonfirmasi" : slot.status}
|
|
</Badge>
|
|
</div>
|
|
<p className="font-medium">
|
|
{new Date(slot.date).toLocaleDateString("id-ID", {
|
|
weekday: "long",
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric"
|
|
})}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} WIB
|
|
</p>
|
|
</div>
|
|
{slot.meet_link && order.payment_status === "paid" && (
|
|
<Button asChild className="shadow-sm">
|
|
<a
|
|
href={slot.meet_link}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<Video className="w-4 h-4 mr-2" />
|
|
Join Meet
|
|
</a>
|
|
</Button>
|
|
)}
|
|
{slot.meet_link && order.payment_status !== "paid" && (
|
|
<p className="text-sm text-muted-foreground">
|
|
Link tersedia setelah pembayaran
|
|
</p>
|
|
)}
|
|
{!slot.meet_link && (
|
|
<p className="text-sm text-muted-foreground">
|
|
Link akan dikirim via email
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</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>
|
|
);
|
|
}
|