Add CSV export functionality for admin orders
- Created exportCSV utility with convertToCSV, downloadCSV, formatExportDate, formatExportIDR - Added export button to AdminOrders page with loading state - Export includes all order fields: ID, email, total, status, payment method, date, refund info - CSV format compatible with Excel and Google Sheets 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
62
src/lib/exportCSV.ts
Normal file
62
src/lib/exportCSV.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Export utility functions for CSV export
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert data array to CSV format
|
||||||
|
*/
|
||||||
|
export const convertToCSV = (data: Record<string, any>[], headers: string[]): string => {
|
||||||
|
// Add headers
|
||||||
|
const csvRows = [headers.join(',')];
|
||||||
|
|
||||||
|
// Add data rows
|
||||||
|
data.forEach((row) => {
|
||||||
|
const values = headers.map((header) => {
|
||||||
|
const value = row[header];
|
||||||
|
// Escape values that contain commas, quotes, or newlines
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const stringValue = String(value);
|
||||||
|
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
|
||||||
|
return `"${stringValue.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return stringValue;
|
||||||
|
});
|
||||||
|
csvRows.push(values.join(','));
|
||||||
|
});
|
||||||
|
|
||||||
|
return csvRows.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger CSV download in browser
|
||||||
|
*/
|
||||||
|
export const downloadCSV = (csv: string, filename: string) => {
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', filename);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date for export (YYYY-MM-DD HH:mm:ss)
|
||||||
|
*/
|
||||||
|
export const formatExportDate = (date: string | Date): string => {
|
||||||
|
const d = typeof date === 'string' ? new Date(date) : date;
|
||||||
|
return d.toISOString().replace('T', ' ').substring(0, 19);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format IDR for export (without "Rp" prefix for easier Excel processing)
|
||||||
|
*/
|
||||||
|
export const formatExportIDR = (amount: number): string => {
|
||||||
|
return (amount / 100).toLocaleString('id-ID');
|
||||||
|
};
|
||||||
@@ -13,9 +13,10 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { formatIDR, formatDateTime } from "@/lib/format";
|
import { formatIDR, formatDateTime } from "@/lib/format";
|
||||||
import { Eye, CheckCircle, XCircle, Video, ExternalLink, Trash2, AlertTriangle, RefreshCw, Link as LinkIcon } from "lucide-react";
|
import { Eye, CheckCircle, XCircle, Video, ExternalLink, Trash2, AlertTriangle, RefreshCw, Link as LinkIcon, Download } from "lucide-react";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
import { getPaymentStatusLabel, getPaymentStatusColor, canRefundOrder, canCancelOrder, canMarkAsPaid } from "@/lib/statusHelpers";
|
import { getPaymentStatusLabel, getPaymentStatusColor, canRefundOrder, canCancelOrder, canMarkAsPaid } from "@/lib/statusHelpers";
|
||||||
|
import { convertToCSV, downloadCSV, formatExportDate, formatExportIDR } from "@/lib/exportCSV";
|
||||||
|
|
||||||
interface Order {
|
interface Order {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -64,6 +65,7 @@ export default function AdminOrders() {
|
|||||||
const [selectedSlotId, setSelectedSlotId] = useState<string | null>(null);
|
const [selectedSlotId, setSelectedSlotId] = useState<string | null>(null);
|
||||||
const [newMeetLink, setNewMeetLink] = useState("");
|
const [newMeetLink, setNewMeetLink] = useState("");
|
||||||
const [creatingMeetLink, setCreatingMeetLink] = useState(false);
|
const [creatingMeetLink, setCreatingMeetLink] = useState(false);
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authLoading) {
|
if (!authLoading) {
|
||||||
@@ -280,6 +282,62 @@ export default function AdminOrders() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExportOrders = async () => {
|
||||||
|
setExporting(true);
|
||||||
|
try {
|
||||||
|
// Fetch all orders with full details
|
||||||
|
const { data: ordersData, error } = await supabase
|
||||||
|
.from("orders")
|
||||||
|
.select("*, profile:profiles(email)")
|
||||||
|
.order("created_at", { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Transform data for CSV export
|
||||||
|
const csvData = (ordersData as Order[]).map((order) => ({
|
||||||
|
"Order ID": order.id,
|
||||||
|
"Email": order.profile?.email || "",
|
||||||
|
"Total": formatExportIDR(order.total_amount),
|
||||||
|
"Status": getPaymentStatusLabel(order.payment_status),
|
||||||
|
"Metode Pembayaran": order.payment_method || "",
|
||||||
|
"Referensi": order.payment_reference || "",
|
||||||
|
"Tanggal": formatExportDate(order.created_at),
|
||||||
|
"Refund Amount": order.refunded_amount ? formatExportIDR(order.refunded_amount) : "",
|
||||||
|
"Refund Reason": order.refund_reason || "",
|
||||||
|
"Refunded At": order.refunded_at ? formatExportDate(order.refunded_at) : "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Convert to CSV
|
||||||
|
const headers = [
|
||||||
|
"Order ID",
|
||||||
|
"Email",
|
||||||
|
"Total",
|
||||||
|
"Status",
|
||||||
|
"Metode Pembayaran",
|
||||||
|
"Referensi",
|
||||||
|
"Tanggal",
|
||||||
|
"Refund Amount",
|
||||||
|
"Refund Reason",
|
||||||
|
"Refunded At",
|
||||||
|
];
|
||||||
|
const csv = convertToCSV(csvData, headers);
|
||||||
|
|
||||||
|
// Download CSV
|
||||||
|
const filename = `orders-${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
downloadCSV(csv, filename);
|
||||||
|
|
||||||
|
toast({ title: "Berhasil", description: "Data order berhasil di-export" });
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Gagal men-export data order",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (authLoading || loading) {
|
if (authLoading || loading) {
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
@@ -294,8 +352,21 @@ export default function AdminOrders() {
|
|||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<div>
|
||||||
<h1 className="text-4xl font-bold mb-2">Manajemen Order</h1>
|
<h1 className="text-4xl font-bold mb-2">Manajemen Order</h1>
|
||||||
<p className="text-muted-foreground mb-8">Kelola semua pesanan</p>
|
<p className="text-muted-foreground">Kelola semua pesanan</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleExportOrders}
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2 border-2"
|
||||||
|
disabled={exporting || orders.length === 0}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
{exporting ? "Men-export..." : "Export Orders"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card className="border-2 border-border hidden md:block">
|
<Card className="border-2 border-border hidden md:block">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
|
|||||||
Reference in New Issue
Block a user