'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 = { PENDING: 'warning', PAID: 'success', WAIVED: 'info', EXPIRED: 'error', }; const Field = ({ label, value, mono = false, truncate = false }: { label: string; value: string; mono?: boolean; truncate?: boolean }) => (

{label}

{value || '\u2014'}

); const SectionHeader = ({ title }: { title: string }) => (

{title}

); export default function PaymentsPage() { const [pageTab, setPageTab] = useState('payments'); const [filters, setFilters] = useState({ search: '', status: '', method: '' }); const [selectedPayment, setSelectedPayment] = useState(null); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [paymentToDelete, setPaymentToDelete] = useState(null); const [deleteError, setDeleteError] = useState(null); const [successMessage, setSuccessMessage] = useState(''); const [exportModalOpen, setExportModalOpen] = useState(false); const [exportDateFrom, setExportDateFrom] = useState(''); const [exportDateTo, setExportDateTo] = useState(''); const [exportColumns, setExportColumns] = useState>({ 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(null); const [suppActionSuccess, setSuppActionSuccess] = useState(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) => {payment.reference || payment.id?.substring(0, 8)} }, { 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) => {payment.method} }, { key: 'status', label: 'Status', render: (payment: any) => {payment.status} }, { 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 (

Payments

Manage payment transactions and refunds

{pageTab === 'supplementary' && ( setSupplementaryOpen(true)}>Raise Charge )} {pageTab === 'payments' && ( setExportModalOpen(true)}>Export )}
{/* Page-level tabs */}
{(['payments', 'supplementary'] as PageTab[]).map((t) => ( ))}
{/* ── SUPPLEMENTARY TAB ── */} {pageTab === 'supplementary' && (
{suppActionSuccess && (
✓ {suppActionSuccess}
)} {suppActionError && (
{suppActionError}
)}
setSuppFilters({ ...suppFilters, bookingRef: e.target.value })} />
{loadingCharges ? (

Loading…

) : charges.length === 0 ? (

No supplementary charges found.

) : (
{charges.map((c: any) => ( ))}
Booking Amount Reason Status Created Actions
{c.booking?.bookingRef ?? c.bookingId.substring(0, 8)} {formatCurrency(c.amountMinor, c.currency ?? 'ETB')} {c.reason} {c.status} {formatDateTime(c.createdAt)} {c.status === 'PENDING' && (
)}
)}
)} {/* ── PAYMENTS TAB ── */} {pageTab === 'payments' && ( <>
{successMessage && (
✓ {successMessage}
)}
setFilters({ ...filters, search: e.target.value })} />
{/* Payment Details Modal */} setSelectedPayment(null)} title="Payment Details" size="xl"> {selectedPayment && (() => { const p = selectedPayment; const statusGrad: Record = { 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 (

Payment Reference

{p.reference || p.id?.substring(0, 8)}

{p.status}

{formatDateTime(p.createdAt)}

{[ { label: 'Amount', value: formatCurrency(p.booking?.totalMinor ?? p.amountMinor, 'ETB') }, { label: 'Method', value: p.method || '—' }, { label: 'Booking', value: p.booking?.bookingRef || '—' }, ].map(({ label, value }) => (

{label}

{value}

))}

Amount

{formatCurrency(p.booking?.totalMinor ?? p.amountMinor, 'ETB')}

{(p.failureReason || p.failureCode) && (
)}
setSelectedPayment(null)}>Close
); })()}
{ 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} /> )} setSupplementaryOpen(false)} /> {/* Export Modal */} setExportModalOpen(false)} title="Export Payments" size="md">
setExportDateFrom(e.target.value)} />
setExportDateTo(e.target.value)} />

Select Columns

{[ { 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) => ( ))}
setExportModalOpen(false)}>Cancel Export CSV
); }