mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
481 lines
23 KiB
TypeScript
481 lines
23 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { Download, Eye, Trash2, AlertCircle, Send, CheckCircle, XCircle, RotateCcw } from 'lucide-react';
|
|
import DataTable from '@/components/ui/DataTable';
|
|
import Badge from '@/components/ui/Badge';
|
|
import ActionButton from '@/components/ui/ActionButton';
|
|
import Modal from '@/components/ui/Modal';
|
|
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
import { paymentsApi, apiClient } from '@/lib/api';
|
|
import Pagination from '@/components/ui/Pagination';
|
|
import { usePagination } from '@/lib/use-pagination';
|
|
import { formatDateTime, formatCurrency } from '@/lib/utils';
|
|
import SupplementaryChargesModal from './SupplementaryChargesModal';
|
|
import {
|
|
useSupplementaryCharges,
|
|
useMarkSupplementaryPaid,
|
|
useWaiveSupplementaryCharge,
|
|
useResendSupplementaryLink,
|
|
} from './useSupplementaryCharges';
|
|
|
|
type PageTab = 'payments' | 'supplementary';
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
PENDING: 'warning',
|
|
PAID: 'success',
|
|
WAIVED: 'info',
|
|
EXPIRED: 'error',
|
|
};
|
|
|
|
const Field = ({ label, value, mono = false, truncate = false }: { label: string; value: string; mono?: boolean; truncate?: boolean }) => (
|
|
<div className="bg-muted/40 rounded-lg p-3">
|
|
<p className="text-xs text-muted-foreground mb-1">{label}</p>
|
|
<p className={`text-sm font-semibold text-foreground${mono ? ' font-mono' : ''}${truncate ? ' truncate' : ''}`} title={value}>{value || '\u2014'}</p>
|
|
</div>
|
|
);
|
|
|
|
const SectionHeader = ({ title }: { title: string }) => (
|
|
<h3 className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-3 flex items-center gap-2">
|
|
<span className="w-4 h-px bg-muted-foreground/40 inline-block" />{title}
|
|
</h3>
|
|
);
|
|
|
|
export default function PaymentsPage() {
|
|
const [pageTab, setPageTab] = useState<PageTab>('payments');
|
|
const [filters, setFilters] = useState({ search: '', status: '', method: '' });
|
|
const [selectedPayment, setSelectedPayment] = useState<any>(null);
|
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
|
const [paymentToDelete, setPaymentToDelete] = useState<any>(null);
|
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
const [successMessage, setSuccessMessage] = useState('');
|
|
const [exportModalOpen, setExportModalOpen] = useState(false);
|
|
const [exportDateFrom, setExportDateFrom] = useState('');
|
|
const [exportDateTo, setExportDateTo] = useState('');
|
|
const [exportColumns, setExportColumns] = useState<Record<string, boolean>>({
|
|
reference: true, booking: true, amount: true, method: true, status: true, createdAt: true,
|
|
});
|
|
const [supplementaryOpen, setSupplementaryOpen] = useState(false);
|
|
|
|
// Supplementary tab state
|
|
const [suppFilters, setSuppFilters] = useState({ bookingRef: '', status: '' });
|
|
const [suppActionError, setSuppActionError] = useState<string | null>(null);
|
|
const [suppActionSuccess, setSuppActionSuccess] = useState<string | null>(null);
|
|
const { data: chargesData, isLoading: loadingCharges } = useSupplementaryCharges(suppFilters);
|
|
const charges: any[] = (chargesData as any)?.items ?? (Array.isArray(chargesData) ? chargesData : []);
|
|
const markPaidMutation = useMarkSupplementaryPaid();
|
|
const waiveMutation = useWaiveSupplementaryCharge();
|
|
const resendMutation = useResendSupplementaryLink();
|
|
|
|
const flashSupp = (msg: string) => { setSuppActionSuccess(msg); setTimeout(() => setSuppActionSuccess(null), 3000); };
|
|
const handleMarkPaid = async (id: string) => {
|
|
setSuppActionError(null);
|
|
try { await markPaidMutation.mutateAsync({ id }); flashSupp('Marked as paid'); }
|
|
catch (e: any) { setSuppActionError(e?.response?.data?.message ?? e?.message ?? 'Failed'); }
|
|
};
|
|
const handleWaive = async (id: string) => {
|
|
setSuppActionError(null);
|
|
try { await waiveMutation.mutateAsync({ id }); flashSupp('Charge waived'); }
|
|
catch (e: any) { setSuppActionError(e?.response?.data?.message ?? e?.message ?? 'Failed'); }
|
|
};
|
|
const handleResend = async (id: string) => {
|
|
setSuppActionError(null);
|
|
try { await resendMutation.mutateAsync(id); flashSupp('Payment link resent'); }
|
|
catch (e: any) { setSuppActionError(e?.response?.data?.message ?? e?.message ?? 'Failed'); }
|
|
};
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => apiClient.delete(`/payments/${id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['payments'] });
|
|
setDeleteConfirmOpen(false);
|
|
setPaymentToDelete(null);
|
|
setDeleteError(null);
|
|
setSuccessMessage('Payment deleted successfully');
|
|
setTimeout(() => setSuccessMessage(''), 3000);
|
|
},
|
|
onError: (error: any) => {
|
|
setDeleteError(error?.response?.data?.message || error?.message || 'Failed to delete payment');
|
|
},
|
|
});
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['payments', filters],
|
|
queryFn: () => paymentsApi.getAll({
|
|
search: filters.search || undefined,
|
|
status: filters.status || undefined,
|
|
method: filters.method || undefined,
|
|
}),
|
|
});
|
|
|
|
const allPayments = (data as any)?.items || (Array.isArray(data) ? data : []);
|
|
const { paged: pagedPayments, page: paymentsPage, totalPages: paymentsTotalPages, setPage: setPaymentsPage } = usePagination(allPayments, 20);
|
|
|
|
const PAYMENT_COLS = [
|
|
{ key: 'reference', label: 'Reference' },
|
|
{ key: 'booking', label: 'Booking Reference' },
|
|
{ key: 'amount', label: 'Amount' },
|
|
{ key: 'method', label: 'Payment Method' },
|
|
{ key: 'status', label: 'Status' },
|
|
{ key: 'createdAt', label: 'Created At' },
|
|
];
|
|
|
|
const confirmExport = () => {
|
|
const cols = Object.entries(exportColumns).filter(([, v]) => v).map(([k]) => k);
|
|
if (cols.length === 0) { alert('Please select at least one column'); return; }
|
|
|
|
const items = ((data as any)?.items || (Array.isArray(data) ? data : [])) as any[];
|
|
const exportItems = items.filter((p: any) => {
|
|
if (!exportDateFrom && !exportDateTo) return true;
|
|
const d = p.createdAt ? new Date(p.createdAt).toISOString().split('T')[0] : null;
|
|
if (exportDateFrom && (!d || d < exportDateFrom)) return false;
|
|
if (exportDateTo && (!d || d > exportDateTo)) return false;
|
|
return true;
|
|
});
|
|
|
|
const csv = [
|
|
PAYMENT_COLS.map(c => `"${c.label}"`).join(','),
|
|
...exportItems.map((payment: any) => {
|
|
const values = PAYMENT_COLS.filter(c => cols.includes(c.key)).map(({ key }) => {
|
|
switch (key) {
|
|
case 'reference': return payment.reference || payment.id?.substring(0, 8) || '';
|
|
case 'booking': return payment.booking?.bookingRef || 'N/A';
|
|
case 'amount': return formatCurrency(payment.booking?.totalMinor ?? payment.amountMinor, 'ETB');
|
|
case 'method': return payment.method || '';
|
|
case 'status': return payment.status || '';
|
|
case 'createdAt': return payment.createdAt ? new Date(payment.createdAt).toLocaleString() : '';
|
|
default: return '';
|
|
}
|
|
});
|
|
return values.map(v => `"${v}"`).join(',');
|
|
}),
|
|
].join('\n');
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `payments-${new Date().toISOString().split('T')[0]}.csv`;
|
|
a.click();
|
|
setExportModalOpen(false);
|
|
};
|
|
|
|
const columns = [
|
|
{ key: 'reference', label: 'Reference', render: (payment: any) => <span className="font-mono">{payment.reference || payment.id?.substring(0, 8)}</span> },
|
|
{ key: 'booking', label: 'Booking', render: (payment: any) => payment.booking?.bookingRef || 'N/A' },
|
|
{ key: 'amount', label: 'Amount', render: (payment: any) => formatCurrency(payment.booking?.totalMinor ?? payment.amountMinor, 'ETB') },
|
|
{ key: 'method', label: 'Method', render: (payment: any) => <Badge>{payment.method}</Badge> },
|
|
{ key: 'status', label: 'Status', render: (payment: any) => <Badge variant="status" status={payment.status}>{payment.status}</Badge> },
|
|
{ key: 'createdAt', label: 'Created', render: (payment: any) => formatDateTime(payment.createdAt) },
|
|
];
|
|
|
|
const paymentActions = [
|
|
{ label: 'View Details', onClick: (p: any) => setSelectedPayment(p), variant: 'secondary' as const, icon: Eye },
|
|
{ label: 'Delete', onClick: (p: any) => { setDeleteError(null); setPaymentToDelete(p); setDeleteConfirmOpen(true); }, variant: 'danger' as const, icon: Trash2 },
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-foreground">Payments</h1>
|
|
<p className="text-muted-foreground">Manage payment transactions and refunds</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{pageTab === 'supplementary' && (
|
|
<ActionButton icon={AlertCircle} variant="secondary" onClick={() => setSupplementaryOpen(true)}>Raise Charge</ActionButton>
|
|
)}
|
|
{pageTab === 'payments' && (
|
|
<ActionButton icon={Download} variant="export" onClick={() => setExportModalOpen(true)}>Export</ActionButton>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Page-level tabs */}
|
|
<div className="flex gap-1 border-b border-muted">
|
|
{(['payments', 'supplementary'] as PageTab[]).map((t) => (
|
|
<button
|
|
key={t}
|
|
onClick={() => setPageTab(t)}
|
|
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
pageTab === t
|
|
? 'border-emerald-500 text-emerald-600 dark:text-emerald-400'
|
|
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
{t === 'payments' ? 'Payments' : 'Supplementary Charges'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* ── SUPPLEMENTARY TAB ── */}
|
|
{pageTab === 'supplementary' && (
|
|
<div className="space-y-4">
|
|
{suppActionSuccess && (
|
|
<div className="rounded-lg bg-green-50 dark:bg-green-900/20 p-3 text-sm text-green-800 dark:text-green-200">✓ {suppActionSuccess}</div>
|
|
)}
|
|
{suppActionError && (
|
|
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-3 text-sm text-red-700 dark:text-red-300">{suppActionError}</div>
|
|
)}
|
|
<div className="card grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="label">Booking Ref</label>
|
|
<input className="input" placeholder="Search booking ref…" value={suppFilters.bookingRef} onChange={(e) => setSuppFilters({ ...suppFilters, bookingRef: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<label className="label">Status</label>
|
|
<select className="input" value={suppFilters.status} onChange={(e) => setSuppFilters({ ...suppFilters, status: e.target.value })}>
|
|
<option value="">All</option>
|
|
<option value="PENDING">Pending</option>
|
|
<option value="PAID">Paid</option>
|
|
<option value="WAIVED">Waived</option>
|
|
<option value="EXPIRED">Expired</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
{loadingCharges ? (
|
|
<p className="text-sm text-muted-foreground py-6 text-center">Loading…</p>
|
|
) : charges.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground py-6 text-center">No supplementary charges found.</p>
|
|
) : (
|
|
<div className="overflow-x-auto rounded-lg border border-muted">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="bg-muted/40 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
<th className="px-3 py-2">Booking</th>
|
|
<th className="px-3 py-2">Amount</th>
|
|
<th className="px-3 py-2">Reason</th>
|
|
<th className="px-3 py-2">Status</th>
|
|
<th className="px-3 py-2">Created</th>
|
|
<th className="px-3 py-2">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-muted">
|
|
{charges.map((c: any) => (
|
|
<tr key={c.id} className="hover:bg-muted/20 transition-colors">
|
|
<td className="px-3 py-2 font-mono text-xs">{c.booking?.bookingRef ?? c.bookingId.substring(0, 8)}</td>
|
|
<td className="px-3 py-2 font-semibold">{formatCurrency(c.amountMinor, c.currency ?? 'ETB')}</td>
|
|
<td className="px-3 py-2 text-xs">{c.reason}</td>
|
|
<td className="px-3 py-2"><Badge variant="status" status={STATUS_COLORS[c.status] ?? c.status}>{c.status}</Badge></td>
|
|
<td className="px-3 py-2 text-xs text-muted-foreground">{formatDateTime(c.createdAt)}</td>
|
|
<td className="px-3 py-2">
|
|
{c.status === 'PENDING' && (
|
|
<div className="flex gap-1">
|
|
<button title="Mark paid" onClick={() => handleMarkPaid(c.id)} className="p-1 rounded hover:bg-green-100 dark:hover:bg-green-900/30 text-green-600"><CheckCircle size={15} /></button>
|
|
<button title="Waive" onClick={() => handleWaive(c.id)} className="p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-red-500"><XCircle size={15} /></button>
|
|
<button title="Resend link" onClick={() => handleResend(c.id)} className="p-1 rounded hover:bg-blue-100 dark:hover:bg-blue-900/30 text-blue-500"><RotateCcw size={15} /></button>
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── PAYMENTS TAB ── */}
|
|
{pageTab === 'payments' && (
|
|
<>
|
|
<div className="card">
|
|
{successMessage && (
|
|
<div className="mb-4 rounded-lg bg-green-50 dark:bg-green-900/20 p-4 text-sm text-green-800 dark:text-green-200">✓ {successMessage}</div>
|
|
)}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="label">Search</label>
|
|
<input type="text" placeholder="Search..." className="input" value={filters.search} onChange={(e) => setFilters({ ...filters, search: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<label className="label">Status</label>
|
|
<select className="input" value={filters.status} onChange={(e) => setFilters({ ...filters, status: e.target.value })}>
|
|
<option value="">All Status</option>
|
|
<option value="PENDING">Pending</option>
|
|
<option value="COMPLETED">Completed</option>
|
|
<option value="FAILED">Failed</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Method</label>
|
|
<select className="input" value={filters.method} onChange={(e) => setFilters({ ...filters, method: e.target.value })}>
|
|
<option value="">All Methods</option>
|
|
<option value="TELEBIRR">Telebirr</option>
|
|
<option value="CBE_BIRR">CBE Birr</option>
|
|
<option value="EBIRR">eBirr</option>
|
|
<option value="CARD">Card</option>
|
|
<option value="WALLET">Wallet</option>
|
|
<option value="CASH">Cash</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
data={pagedPayments}
|
|
columns={columns}
|
|
actions={paymentActions}
|
|
loading={isLoading}
|
|
emptyMessage="No payments found"
|
|
/>
|
|
<Pagination currentPage={paymentsPage} totalPages={paymentsTotalPages} onPageChange={setPaymentsPage} />
|
|
|
|
{/* Payment Details Modal */}
|
|
<Modal isOpen={!!selectedPayment} onClose={() => setSelectedPayment(null)} title="Payment Details" size="xl">
|
|
{selectedPayment && (() => {
|
|
const p = selectedPayment;
|
|
const statusGrad: Record<string, string> = {
|
|
COMPLETED: 'from-emerald-600 to-emerald-700',
|
|
FAILED: 'from-red-600 to-red-700',
|
|
PENDING: 'from-amber-500 to-amber-600',
|
|
REFUNDED: 'from-blue-600 to-blue-700',
|
|
};
|
|
const grad = statusGrad[p.status] || 'from-gray-600 to-gray-700';
|
|
return (
|
|
<div>
|
|
<div className={`-mx-6 -mt-4 mb-6 px-6 py-5 bg-gradient-to-r ${grad} rounded-t-lg`}>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<p className="text-white/70 text-xs font-semibold uppercase tracking-widest mb-1">Payment Reference</p>
|
|
<p className="text-white text-2xl font-mono font-bold">{p.reference || p.id?.substring(0, 8)}</p>
|
|
</div>
|
|
<div className="text-right shrink-0">
|
|
<Badge variant="status" status={p.status}>{p.status}</Badge>
|
|
<p className="text-white/70 text-xs mt-1">{formatDateTime(p.createdAt)}</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 grid grid-cols-3 gap-3">
|
|
{[
|
|
{ label: 'Amount', value: formatCurrency(p.booking?.totalMinor ?? p.amountMinor, 'ETB') },
|
|
{ label: 'Method', value: p.method || '—' },
|
|
{ label: 'Booking', value: p.booking?.bookingRef || '—' },
|
|
].map(({ label, value }) => (
|
|
<div key={label} className="bg-white/10 rounded-lg px-3 py-2">
|
|
<p className="text-white/70 text-xs">{label}</p>
|
|
<p className="text-white text-sm font-bold truncate">{value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<section>
|
|
<SectionHeader title="Transaction" />
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<div className="bg-muted/40 rounded-lg p-3 col-span-2">
|
|
<p className="text-xs text-muted-foreground mb-1">Amount</p>
|
|
<p className="text-xl font-bold">{formatCurrency(p.booking?.totalMinor ?? p.amountMinor, 'ETB')}</p>
|
|
</div>
|
|
<Field label="Method" value={p.method} />
|
|
<Field label="Status" value={p.status} />
|
|
<Field label="Reference" value={p.reference} mono truncate />
|
|
<Field label="Provider Ref" value={p.providerReference || p.externalReference} mono truncate />
|
|
<Field label="Created" value={formatDateTime(p.createdAt)} />
|
|
<Field label="Completed At" value={p.completedAt ? formatDateTime(p.completedAt) : 'N/A'} />
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<SectionHeader title="Booking" />
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<Field label="Booking Ref" value={p.booking?.bookingRef} mono />
|
|
<Field label="Booking Status" value={p.booking?.status} />
|
|
<Field label="Passenger" value={p.booking?.passenger?.fullName || p.booking?.contactEmail} truncate />
|
|
<Field label="Booking ID" value={p.bookingId} mono truncate />
|
|
</div>
|
|
</section>
|
|
|
|
{(p.failureReason || p.failureCode) && (
|
|
<section>
|
|
<SectionHeader title="Failure Information" />
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<Field label="Failure Code" value={p.failureCode} mono />
|
|
<Field label="Failure Reason" value={p.failureReason} truncate />
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<section>
|
|
<SectionHeader title="IDs" />
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<Field label="Payment ID" value={p.id} mono truncate />
|
|
<Field label="Last Updated" value={formatDateTime(p.updatedAt)} />
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-6 mt-2 border-t border-muted">
|
|
<ActionButton variant="secondary" onClick={() => setSelectedPayment(null)}>Close</ActionButton>
|
|
</div>
|
|
</div>
|
|
);
|
|
})()}
|
|
</Modal>
|
|
|
|
<ConfirmDialog
|
|
isOpen={deleteConfirmOpen}
|
|
onClose={() => { setDeleteConfirmOpen(false); setPaymentToDelete(null); setDeleteError(null); }}
|
|
onConfirm={async () => { if (paymentToDelete) await deleteMutation.mutateAsync(paymentToDelete.id); }}
|
|
title="Delete Payment"
|
|
message={`Permanently delete payment ${paymentToDelete?.reference || paymentToDelete?.id?.substring(0, 8)}? This cannot be undone.`}
|
|
confirmText="Delete" cancelText="Cancel" isLoading={deleteMutation.isPending} isDanger
|
|
error={deleteError ?? undefined}
|
|
/>
|
|
|
|
</>
|
|
)}
|
|
|
|
<SupplementaryChargesModal isOpen={supplementaryOpen} onClose={() => setSupplementaryOpen(false)} />
|
|
|
|
{/* Export Modal */}
|
|
<Modal isOpen={exportModalOpen} onClose={() => setExportModalOpen(false)} title="Export Payments" size="md">
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Date From</label>
|
|
<input type="date" className="input" value={exportDateFrom} onChange={(e) => setExportDateFrom(e.target.value)} />
|
|
</div>
|
|
<div>
|
|
<label className="label">Date To</label>
|
|
<input type="date" className="input" value={exportDateTo} onChange={(e) => setExportDateTo(e.target.value)} />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-sm font-medium mb-2">Select Columns</p>
|
|
<div className="space-y-2">
|
|
{[
|
|
{ key: 'reference', label: 'Reference' },
|
|
{ key: 'booking', label: 'Booking Reference' },
|
|
{ key: 'amount', label: 'Amount' },
|
|
{ key: 'method', label: 'Payment Method' },
|
|
{ key: 'status', label: 'Status' },
|
|
{ key: 'createdAt', label: 'Created At' },
|
|
].map((col) => (
|
|
<label key={col.key} className="flex items-center gap-3 p-2 hover:bg-gray-50 dark:hover:bg-gray-900/50 rounded cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={exportColumns[col.key] || false}
|
|
onChange={(e) => setExportColumns({ ...exportColumns, [col.key]: e.target.checked })}
|
|
className="w-4 h-4 rounded border-gray-300"
|
|
/>
|
|
<span className="text-sm font-medium">{col.label}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-4 border-t">
|
|
<ActionButton variant="secondary" onClick={() => setExportModalOpen(false)}>Cancel</ActionButton>
|
|
<ActionButton onClick={confirmExport}>Export CSV</ActionButton>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|