puffin-app/components/admin/ExportButton.tsx

297 lines
8.9 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Download, Loader2 } from 'lucide-react';
import { OrderRecord } from '@/src/types';
interface ExportButtonProps {
currentOrders?: OrderRecord[];
filters?: {
search?: string;
status?: string;
dateFrom?: string;
dateTo?: string;
};
}
export function ExportButton({ currentOrders = [], filters = {} }: ExportButtonProps) {
const [isExporting, setIsExporting] = useState(false);
const [showDropdown, setShowDropdown] = useState(false);
// Close dropdown on escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') setShowDropdown(false);
};
if (showDropdown) {
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}
}, [showDropdown]);
const formatDate = (dateString: string | undefined) => {
if (!dateString) return '';
return new Date(dateString).toISOString();
};
const formatCurrency = (amount: string | undefined) => {
if (!amount) return '';
return (parseFloat(amount) / 100).toFixed(2);
};
const convertToCSV = (orders: OrderRecord[]): string => {
if (orders.length === 0) return '';
// CSV Headers
const headers = [
'Order ID',
'Status',
'Source',
'Created At',
'Updated At',
'Customer Name',
'Customer Email',
'Customer Phone',
'Business Name',
'Tax ID Type',
'Tax ID Value',
'Base Amount',
'Processing Fee',
'Total Amount',
'Currency',
'Payment Method',
'Stripe Session ID',
'Stripe Payment Intent',
'Stripe Customer ID',
'CO2 Tons',
'Portfolio ID',
'Portfolio Name',
'Wren Order ID',
'Certificate URL',
'Fulfilled At',
'Billing Line 1',
'Billing Line 2',
'Billing City',
'Billing State',
'Billing Postal Code',
'Billing Country',
'Vessel Name',
'IMO Number',
'Vessel Type',
'Vessel Length',
'Departure Port',
'Arrival Port',
'Distance',
'Avg Speed',
'Duration',
'Engine Power',
'Notes',
];
// CSV Rows
const rows = orders.map((order) => [
order.orderId,
order.status,
order.source || '',
formatDate(order.CreatedAt),
formatDate(order.UpdatedAt),
order.customerName,
order.customerEmail,
order.customerPhone || '',
order.businessName || '',
order.taxIdType || '',
order.taxIdValue || '',
formatCurrency(order.baseAmount),
formatCurrency(order.processingFee),
formatCurrency(order.totalAmount),
order.currency,
order.paymentMethod || '',
order.stripeSessionId || '',
order.stripePaymentIntent || '',
order.stripeCustomerId || '',
parseFloat(order.co2Tons).toFixed(2),
order.portfolioId,
order.portfolioName || '',
order.wrenOrderId || '',
order.certificateUrl || '',
formatDate(order.fulfilledAt),
order.billingLine1 || '',
order.billingLine2 || '',
order.billingCity || '',
order.billingState || '',
order.billingPostalCode || '',
order.billingCountry || '',
order.vesselName || '',
order.imoNumber || '',
order.vesselType || '',
order.vesselLength || '',
order.departurePort || '',
order.arrivalPort || '',
order.distance || '',
order.avgSpeed || '',
order.duration || '',
order.enginePower || '',
order.notes || '',
]);
// Escape CSV fields (handle commas, quotes, newlines)
const escapeCSVField = (field: string | number) => {
const stringField = String(field);
if (
stringField.includes(',') ||
stringField.includes('"') ||
stringField.includes('\n')
) {
return `"${stringField.replace(/"/g, '""')}"`;
}
return stringField;
};
// Build CSV
const csvContent = [
headers.map(escapeCSVField).join(','),
...rows.map((row) => row.map(escapeCSVField).join(',')),
].join('\n');
return csvContent;
};
const downloadCSV = (csvContent: string, filename: string) => {
const blob = new Blob([csvContent], { 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);
URL.revokeObjectURL(url);
};
const handleExportCurrent = () => {
setIsExporting(true);
setShowDropdown(false);
try {
const csvContent = convertToCSV(currentOrders);
const timestamp = new Date().toISOString().split('T')[0];
const filename = `orders-current-${timestamp}.csv`;
downloadCSV(csvContent, filename);
} catch (error) {
console.error('Export failed:', error);
alert('Failed to export orders. Please try again.');
} finally {
setTimeout(() => setIsExporting(false), 1000);
}
};
const handleExportAll = async () => {
setIsExporting(true);
setShowDropdown(false);
try {
// Build query params with current filters
const params = new URLSearchParams();
params.append('limit', '10000'); // High limit to get all
params.append('offset', '0');
if (filters.search) params.append('search', filters.search);
if (filters.status) params.append('status', filters.status);
if (filters.dateFrom) params.append('dateFrom', filters.dateFrom);
if (filters.dateTo) params.append('dateTo', filters.dateTo);
// Fetch all orders
const response = await fetch(`/api/admin/orders?${params}`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch orders');
}
const allOrders = data.data.list || [];
const csvContent = convertToCSV(allOrders);
const timestamp = new Date().toISOString().split('T')[0];
const filename = `orders-all-${timestamp}.csv`;
downloadCSV(csvContent, filename);
} catch (error) {
console.error('Export failed:', error);
alert('Failed to export all orders. Please try again.');
} finally {
setTimeout(() => setIsExporting(false), 1000);
}
};
return (
<div className="relative inline-block">
<button
type="button"
onClick={() => setShowDropdown(!showDropdown)}
disabled={isExporting}
className="flex items-center space-x-2 px-4 py-2.5 text-sm font-medium text-white bg-deep-sea-blue hover:bg-deep-sea-blue/90 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
title="Export orders to CSV"
>
{isExporting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>Exporting...</span>
</>
) : (
<>
<Download className="w-4 h-4" />
<span>Export</span>
</>
)}
</button>
{/* Dropdown Menu */}
<AnimatePresence>
{showDropdown && (
<>
{/* Backdrop to close dropdown */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-10"
onClick={() => setShowDropdown(false)}
/>
{/* Dropdown */}
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.15 }}
className="absolute right-0 mt-2 w-64 bg-white border border-light-gray-border rounded-xl shadow-lg z-20 overflow-hidden"
>
<button
type="button"
onClick={handleExportCurrent}
className="w-full px-4 py-3 text-left text-sm text-deep-sea-blue hover:bg-gray-50 transition-colors border-b border-gray-100"
>
<div className="font-medium">Export Current Page</div>
<div className="text-xs text-deep-sea-blue/60 mt-0.5">
{currentOrders.length} orders
</div>
</button>
<button
type="button"
onClick={handleExportAll}
className="w-full px-4 py-3 text-left text-sm text-deep-sea-blue hover:bg-gray-50 transition-colors"
>
<div className="font-medium">Export All (with filters)</div>
<div className="text-xs text-deep-sea-blue/60 mt-0.5">
All matching orders as CSV
</div>
</button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}