Fix consulting booking flow and export CSV format
1. CSV Export: Use raw numbers for Total and Refund Amount columns - Changed from formatted IDR (with dots) to plain numbers - Prevents Excel from breaking values with thousand separators 2. Consulting Booking Flow: - Fixed "Booking Sekarang" to navigate to order detail instead of redirecting to Pakasir - Payment QR code now displays in OrderDetail page - Consulting orders show slot details instead of empty items list 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -336,7 +336,7 @@ export default function ConsultingBooking() {
|
|||||||
const { error: slotsError } = await supabase.from('consulting_slots').insert(slotsToInsert);
|
const { error: slotsError } = await supabase.from('consulting_slots').insert(slotsToInsert);
|
||||||
if (slotsError) throw slotsError;
|
if (slotsError) throw slotsError;
|
||||||
|
|
||||||
// Call edge function to create payment (avoids CORS)
|
// Call edge function to create payment with QR code
|
||||||
const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-payment', {
|
const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-payment', {
|
||||||
body: {
|
body: {
|
||||||
order_id: order.id,
|
order_id: order.id,
|
||||||
@@ -351,12 +351,8 @@ export default function ConsultingBooking() {
|
|||||||
throw new Error(paymentError.message || 'Gagal membuat pembayaran');
|
throw new Error(paymentError.message || 'Gagal membuat pembayaran');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paymentData?.success && paymentData?.data?.payment_url) {
|
// Navigate to order detail page to show QR code
|
||||||
// Redirect to payment page
|
navigate(`/orders/${order.id}`);
|
||||||
window.location.href = paymentData.data.payment_url;
|
|
||||||
} else {
|
|
||||||
throw new Error('Gagal membuat URL pembayaran');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -297,12 +297,12 @@ export default function AdminOrders() {
|
|||||||
const csvData = (ordersData as Order[]).map((order) => ({
|
const csvData = (ordersData as Order[]).map((order) => ({
|
||||||
"Order ID": order.id,
|
"Order ID": order.id,
|
||||||
"Email": order.profile?.email || "",
|
"Email": order.profile?.email || "",
|
||||||
"Total": formatExportIDR(order.total_amount),
|
"Total": order.total_amount / 100, // Raw number in IDR (no formatting)
|
||||||
"Status": getPaymentStatusLabel(order.payment_status),
|
"Status": getPaymentStatusLabel(order.payment_status),
|
||||||
"Metode Pembayaran": order.payment_method || "",
|
"Metode Pembayaran": order.payment_method || "",
|
||||||
"Referensi": order.payment_reference || "",
|
"Referensi": order.payment_reference || "",
|
||||||
"Tanggal": formatExportDate(order.created_at),
|
"Tanggal": formatExportDate(order.created_at),
|
||||||
"Refund Amount": order.refunded_amount ? formatExportIDR(order.refunded_amount) : "",
|
"Refund Amount": order.refunded_amount ? order.refunded_amount / 100 : "",
|
||||||
"Refund Reason": order.refund_reason || "",
|
"Refund Reason": order.refund_reason || "",
|
||||||
"Refunded At": order.refunded_at ? formatExportDate(order.refunded_at) : "",
|
"Refunded At": order.refunded_at ? formatExportDate(order.refunded_at) : "",
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -476,8 +476,107 @@ export default function OrderDetail() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Smart Item/Service Display */}
|
{/* Smart Item/Service Display */}
|
||||||
{order.order_items.length > 0 ? (
|
{consultingSlots.length > 0 ? (
|
||||||
// === Product Orders ===
|
// === Consulting Orders (NO order_items, has consulting_slots) ===
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Tanggal</p>
|
||||||
|
<p className="font-medium">
|
||||||
|
{new Date(consultingSlots[0].date).toLocaleDateString("id-ID", {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
})}
|
||||||
|
</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.
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Total */}
|
||||||
|
<div className="flex items-center justify-between text-lg font-bold pt-4 border-t">
|
||||||
|
<span>Total Pembayaran</span>
|
||||||
|
<span>{formatIDR(order.total_amount)}</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Consulting Slots Detail */}
|
||||||
|
<Card className="border-2 border-border mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Detail Jadwal</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{consultingSlots.map((slot, index) => (
|
||||||
|
<div key={slot.id} className="flex items-center justify-between py-2 border-b last:border-b-0">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Blok {index + 1}</p>
|
||||||
|
<p className="font-medium">{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} WIB</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant={slot.status === "confirmed" ? "default" : "secondary"}>
|
||||||
|
{slot.status === "confirmed" ? "Terkonfirmasi" : slot.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
) : order.order_items.length > 0 ? (
|
||||||
|
// === Product Orders (has order_items) ===
|
||||||
<Card className="border-2 border-border mb-6">
|
<Card className="border-2 border-border mb-6">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg flex items-center gap-2">
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
@@ -518,127 +617,8 @@ export default function OrderDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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}
|
) : 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 */}
|
{/* Access Info */}
|
||||||
{order.payment_status === "paid" && (
|
{order.payment_status === "paid" && (
|
||||||
<Card className="border-2 border-primary bg-primary/5">
|
<Card className="border-2 border-primary bg-primary/5">
|
||||||
|
|||||||
Reference in New Issue
Block a user