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:
dwindown
2025-12-26 18:25:43 +07:00
parent 5a05203f2b
commit bf212fb973
2 changed files with 136 additions and 3 deletions

62
src/lib/exportCSV.ts Normal file
View 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');
};

View File

@@ -13,9 +13,10 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
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 { getPaymentStatusLabel, getPaymentStatusColor, canRefundOrder, canCancelOrder, canMarkAsPaid } from "@/lib/statusHelpers";
import { convertToCSV, downloadCSV, formatExportDate, formatExportIDR } from "@/lib/exportCSV";
interface Order {
id: string;
@@ -64,6 +65,7 @@ export default function AdminOrders() {
const [selectedSlotId, setSelectedSlotId] = useState<string | null>(null);
const [newMeetLink, setNewMeetLink] = useState("");
const [creatingMeetLink, setCreatingMeetLink] = useState(false);
const [exporting, setExporting] = useState(false);
useEffect(() => {
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) {
return (
<AppLayout>
@@ -294,8 +352,21 @@ export default function AdminOrders() {
return (
<AppLayout>
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-2">Manajemen Order</h1>
<p className="text-muted-foreground mb-8">Kelola semua pesanan</p>
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-4xl font-bold mb-2">Manajemen Order</h1>
<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">
<CardContent className="p-0">