Files
Spanglish/frontend/src/app/admin/payments/page.tsx
Michilis 23d0325d8d feat: add payment management improvements and reminder emails
- Add option to approve/reject payments without sending notification emails
  (checkbox in review popup, default enabled)
- Add payment reminder email template and send functionality
- Track when reminder emails are sent (reminderSentAt field)
- Display reminder sent timestamp in payment review popup
- Make payment review popup scrollable for better UX
- Add payment-reminder template to email system (available in admin emails)
2026-02-05 04:13:42 +00:00

916 lines
40 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect } from 'react';
import { useLanguage } from '@/context/LanguageContext';
import { paymentsApi, adminApi, eventsApi, PaymentWithDetails, Event, ExportedPayment, FinancialSummary } from '@/lib/api';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import {
CheckCircleIcon,
ArrowPathIcon,
ArrowDownTrayIcon,
DocumentArrowDownIcon,
XCircleIcon,
ClockIcon,
ExclamationTriangleIcon,
ChatBubbleLeftIcon,
BoltIcon,
BanknotesIcon,
BuildingLibraryIcon,
CreditCardIcon,
EnvelopeIcon,
} from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
type Tab = 'pending_approval' | 'all';
export default function AdminPaymentsPage() {
const { t, locale } = useLanguage();
const [payments, setPayments] = useState<PaymentWithDetails[]>([]);
const [pendingApprovalPayments, setPendingApprovalPayments] = useState<PaymentWithDetails[]>([]);
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<Tab>('pending_approval');
const [statusFilter, setStatusFilter] = useState<string>('');
const [providerFilter, setProviderFilter] = useState<string>('');
// Modal state
const [selectedPayment, setSelectedPayment] = useState<PaymentWithDetails | null>(null);
const [noteText, setNoteText] = useState('');
const [processing, setProcessing] = useState(false);
const [sendEmail, setSendEmail] = useState(true);
const [sendingReminder, setSendingReminder] = useState(false);
// Export state
const [showExportModal, setShowExportModal] = useState(false);
const [exporting, setExporting] = useState(false);
const [exportData, setExportData] = useState<{ payments: ExportedPayment[]; summary: FinancialSummary } | null>(null);
const [exportFilters, setExportFilters] = useState({
startDate: '',
endDate: '',
eventId: '',
});
useEffect(() => {
loadData();
}, [statusFilter, providerFilter]);
const loadData = async () => {
try {
setLoading(true);
const [pendingRes, allRes, eventsRes] = await Promise.all([
paymentsApi.getPendingApproval(),
paymentsApi.getAll({
status: statusFilter || undefined,
provider: providerFilter || undefined
}),
eventsApi.getAll(),
]);
setPendingApprovalPayments(pendingRes.payments);
setPayments(allRes.payments);
setEvents(eventsRes.events);
} catch (error) {
toast.error('Failed to load payments');
} finally {
setLoading(false);
}
};
const handleApprove = async (payment: PaymentWithDetails) => {
setProcessing(true);
try {
await paymentsApi.approve(payment.id, noteText, sendEmail);
toast.success(locale === 'es' ? 'Pago aprobado' : 'Payment approved');
setSelectedPayment(null);
setNoteText('');
setSendEmail(true);
loadData();
} catch (error: any) {
toast.error(error.message || 'Failed to approve payment');
} finally {
setProcessing(false);
}
};
const handleReject = async (payment: PaymentWithDetails) => {
setProcessing(true);
try {
await paymentsApi.reject(payment.id, noteText, sendEmail);
toast.success(locale === 'es' ? 'Pago rechazado' : 'Payment rejected');
setSelectedPayment(null);
setNoteText('');
setSendEmail(true);
loadData();
} catch (error: any) {
toast.error(error.message || 'Failed to reject payment');
} finally {
setProcessing(false);
}
};
const handleSendReminder = async (payment: PaymentWithDetails) => {
setSendingReminder(true);
try {
const result = await paymentsApi.sendReminder(payment.id);
toast.success(locale === 'es' ? 'Recordatorio enviado' : 'Reminder sent');
// Update the selected payment with the new reminderSentAt timestamp
if (result.reminderSentAt) {
setSelectedPayment({ ...payment, reminderSentAt: result.reminderSentAt });
}
// Also refresh the data to update the lists
loadData();
} catch (error: any) {
toast.error(error.message || 'Failed to send reminder');
} finally {
setSendingReminder(false);
}
};
const handleConfirmPayment = async (id: string) => {
try {
await paymentsApi.approve(id);
toast.success('Payment confirmed');
loadData();
} catch (error) {
toast.error('Failed to confirm payment');
}
};
const handleRefund = async (id: string) => {
if (!confirm('Are you sure you want to process this refund?')) return;
try {
await paymentsApi.refund(id);
toast.success('Refund processed');
loadData();
} catch (error: any) {
toast.error(error.message || 'Failed to process refund');
}
};
const handleExport = async () => {
setExporting(true);
try {
const data = await adminApi.exportFinancial({
startDate: exportFilters.startDate || undefined,
endDate: exportFilters.endDate || undefined,
eventId: exportFilters.eventId || undefined,
});
setExportData(data);
} catch (error) {
toast.error('Failed to generate export');
} finally {
setExporting(false);
}
};
const downloadCSV = () => {
if (!exportData) return;
const headers = ['Payment ID', 'Amount', 'Currency', 'Provider', 'Status', 'Reference', 'Paid At', 'Created At', 'Attendee Name', 'Attendee Email', 'Event Title', 'Event Date'];
const rows = exportData.payments.map(p => [
p.paymentId,
p.amount,
p.currency,
p.provider,
p.status,
p.reference || '',
p.paidAt || '',
p.createdAt,
`${p.attendeeFirstName} ${p.attendeeLastName || ''}`.trim(),
p.attendeeEmail || '',
p.eventTitle,
p.eventDate,
]);
const csvContent = [headers, ...rows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `financial-export-${new Date().toISOString().split('T')[0]}.csv`;
link.click();
toast.success('CSV downloaded');
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
const formatCurrency = (amount: number, currency: string) => {
return `${amount.toLocaleString()} ${currency}`;
};
const getStatusBadge = (status: string) => {
const styles: Record<string, string> = {
pending: 'bg-gray-100 text-gray-700',
pending_approval: 'bg-yellow-100 text-yellow-700',
paid: 'bg-green-100 text-green-700',
refunded: 'bg-blue-100 text-blue-700',
failed: 'bg-red-100 text-red-700',
cancelled: 'bg-gray-100 text-gray-700',
};
const labels: Record<string, string> = {
pending: locale === 'es' ? 'Pendiente' : 'Pending',
pending_approval: locale === 'es' ? 'Esperando Aprobación' : 'Pending Approval',
paid: locale === 'es' ? 'Pagado' : 'Paid',
refunded: locale === 'es' ? 'Reembolsado' : 'Refunded',
failed: locale === 'es' ? 'Fallido' : 'Failed',
cancelled: locale === 'es' ? 'Cancelado' : 'Cancelled',
};
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${styles[status] || 'bg-gray-100 text-gray-700'}`}>
{labels[status] || status}
</span>
);
};
const getProviderIcon = (provider: string) => {
const icons: Record<string, typeof BoltIcon> = {
lightning: BoltIcon,
cash: BanknotesIcon,
bank_transfer: BuildingLibraryIcon,
tpago: CreditCardIcon,
bancard: CreditCardIcon,
};
const Icon = icons[provider] || CreditCardIcon;
return <Icon className="w-4 h-4" />;
};
const getProviderLabel = (provider: string) => {
const labels: Record<string, string> = {
cash: locale === 'es' ? 'Efectivo' : 'Cash',
bank_transfer: locale === 'es' ? 'Transferencia Bancaria' : 'Bank Transfer',
lightning: 'Lightning',
tpago: 'TPago',
bancard: 'Bancard',
};
return labels[provider] || provider;
};
// Helper to get booking info for a payment (ticket count and total)
const getBookingInfo = (payment: PaymentWithDetails) => {
if (!payment.ticket?.bookingId) {
return { ticketCount: 1, bookingTotal: payment.amount };
}
// Count all payments with the same bookingId
const bookingPayments = payments.filter(
p => p.ticket?.bookingId === payment.ticket?.bookingId
);
return {
ticketCount: bookingPayments.length,
bookingTotal: bookingPayments.reduce((sum, p) => sum + Number(p.amount), 0),
};
};
// Get booking info for pending approval payments
const getPendingBookingInfo = (payment: PaymentWithDetails) => {
if (!payment.ticket?.bookingId) {
return { ticketCount: 1, bookingTotal: payment.amount };
}
// Count all pending payments with the same bookingId
const bookingPayments = pendingApprovalPayments.filter(
p => p.ticket?.bookingId === payment.ticket?.bookingId
);
return {
ticketCount: bookingPayments.length,
bookingTotal: bookingPayments.reduce((sum, p) => sum + Number(p.amount), 0),
};
};
// Calculate totals (sum all individual payment amounts)
const totalPending = payments
.filter(p => p.status === 'pending' || p.status === 'pending_approval')
.reduce((sum, p) => sum + Number(p.amount), 0);
const totalPaid = payments
.filter(p => p.status === 'paid')
.reduce((sum, p) => sum + Number(p.amount), 0);
// Get unique booking count (for summary display)
const getUniqueBookingsCount = (paymentsList: PaymentWithDetails[]) => {
const seen = new Set<string>();
let count = 0;
paymentsList.forEach(p => {
const bookingKey = p.ticket?.bookingId || p.id;
if (!seen.has(bookingKey)) {
seen.add(bookingKey);
count++;
}
});
return count;
};
const pendingBookingsCount = getUniqueBookingsCount(
payments.filter(p => p.status === 'pending' || p.status === 'pending_approval')
);
const paidBookingsCount = getUniqueBookingsCount(
payments.filter(p => p.status === 'paid')
);
const pendingApprovalBookingsCount = getUniqueBookingsCount(pendingApprovalPayments);
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full" />
</div>
);
}
return (
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-primary-dark">{t('admin.payments.title')}</h1>
<Button onClick={() => setShowExportModal(true)}>
<DocumentArrowDownIcon className="w-5 h-5 mr-2" />
{locale === 'es' ? 'Exportar Datos' : 'Export Data'}
</Button>
</div>
{/* Approval Detail Modal */}
{selectedPayment && (() => {
const modalBookingInfo = getBookingInfo(selectedPayment);
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<Card className="w-full max-w-lg max-h-[90vh] overflow-y-auto p-6">
<h2 className="text-xl font-bold mb-4">
{locale === 'es' ? 'Verificar Pago' : 'Verify Payment'}
</h2>
<div className="space-y-4 mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-gray-500">{locale === 'es' ? 'Monto Total' : 'Total Amount'}</p>
<p className="font-bold text-lg">{formatCurrency(modalBookingInfo.bookingTotal, selectedPayment.currency)}</p>
{modalBookingInfo.ticketCount > 1 && (
<div className="mt-2 p-2 bg-purple-50 rounded">
<p className="text-xs text-purple-700">
📦 {modalBookingInfo.ticketCount} tickets × {formatCurrency(selectedPayment.amount, selectedPayment.currency)}
</p>
</div>
)}
</div>
<div>
<p className="text-gray-500">{locale === 'es' ? 'Método' : 'Method'}</p>
<p className="font-medium flex items-center gap-2">
{getProviderIcon(selectedPayment.provider)}
{getProviderLabel(selectedPayment.provider)}
</p>
</div>
</div>
</div>
{selectedPayment.ticket && (
<div className="border rounded-lg p-4">
<h4 className="font-medium mb-2">{locale === 'es' ? 'Asistente' : 'Attendee'}</h4>
<p className="font-bold">
{selectedPayment.ticket.attendeeFirstName} {selectedPayment.ticket.attendeeLastName}
</p>
<p className="text-sm text-gray-600">{selectedPayment.ticket.attendeeEmail}</p>
{selectedPayment.ticket.attendeePhone && (
<p className="text-sm text-gray-600">{selectedPayment.ticket.attendeePhone}</p>
)}
</div>
)}
{selectedPayment.event && (
<div className="border rounded-lg p-4">
<h4 className="font-medium mb-2">{locale === 'es' ? 'Evento' : 'Event'}</h4>
<p className="font-bold">{selectedPayment.event.title}</p>
<p className="text-sm text-gray-600">{formatDate(selectedPayment.event.startDatetime)}</p>
</div>
)}
{selectedPayment.userMarkedPaidAt && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<ClockIcon className="w-4 h-4" />
{locale === 'es' ? 'Usuario marcó como pagado:' : 'User marked as paid:'} {formatDate(selectedPayment.userMarkedPaidAt)}
</div>
)}
{selectedPayment.reminderSentAt && (
<div className="flex items-center gap-2 text-sm text-amber-600">
<EnvelopeIcon className="w-4 h-4" />
{locale === 'es' ? 'Recordatorio enviado:' : 'Reminder sent:'} {formatDate(selectedPayment.reminderSentAt)}
</div>
)}
{selectedPayment.payerName && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
<p className="text-sm text-amber-800 font-medium">
{locale === 'es' ? '⚠️ Pagado por otra persona:' : '⚠️ Paid by someone else:'}
</p>
<p className="text-amber-900 font-bold">{selectedPayment.payerName}</p>
</div>
)}
<div>
<label className="block text-sm font-medium mb-1">
{locale === 'es' ? 'Nota interna (opcional)' : 'Internal note (optional)'}
</label>
<textarea
value={noteText}
onChange={(e) => setNoteText(e.target.value)}
rows={2}
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
placeholder={locale === 'es' ? 'Agregar nota...' : 'Add a note...'}
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="sendEmail"
checked={sendEmail}
onChange={(e) => setSendEmail(e.target.checked)}
className="w-4 h-4 text-primary-yellow border-gray-300 rounded focus:ring-primary-yellow"
/>
<label htmlFor="sendEmail" className="text-sm text-gray-700">
{locale === 'es' ? 'Enviar email de notificación' : 'Send notification email'}
</label>
</div>
</div>
<div className="flex gap-3">
<Button
onClick={() => handleApprove(selectedPayment)}
isLoading={processing}
className="flex-1"
>
<CheckCircleIcon className="w-5 h-5 mr-2" />
{locale === 'es' ? 'Aprobar' : 'Approve'}
</Button>
<Button
variant="outline"
onClick={() => handleReject(selectedPayment)}
isLoading={processing}
className="flex-1 border-red-300 text-red-600 hover:bg-red-50"
>
<XCircleIcon className="w-5 h-5 mr-2" />
{locale === 'es' ? 'Rechazar' : 'Reject'}
</Button>
</div>
<div className="pt-2 border-t">
<Button
variant="outline"
onClick={() => handleSendReminder(selectedPayment)}
isLoading={sendingReminder}
className="w-full"
>
<EnvelopeIcon className="w-5 h-5 mr-2" />
{locale === 'es' ? 'Enviar recordatorio de pago' : 'Send payment reminder'}
</Button>
</div>
<button
onClick={() => { setSelectedPayment(null); setNoteText(''); setSendEmail(true); }}
className="w-full mt-3 py-2 text-sm text-gray-500 hover:text-gray-700"
>
{locale === 'es' ? 'Cancelar' : 'Cancel'}
</button>
</Card>
</div>
);
})()}
{/* Export Modal */}
{showExportModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<Card className="w-full max-w-2xl max-h-[90vh] overflow-y-auto p-6">
<h2 className="text-xl font-bold mb-6">{locale === 'es' ? 'Exportar Datos Financieros' : 'Export Financial Data'}</h2>
{!exportData ? (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label={locale === 'es' ? 'Fecha Inicio' : 'Start Date'}
type="date"
value={exportFilters.startDate}
onChange={(e) => setExportFilters({ ...exportFilters, startDate: e.target.value })}
/>
<Input
label={locale === 'es' ? 'Fecha Fin' : 'End Date'}
type="date"
value={exportFilters.endDate}
onChange={(e) => setExportFilters({ ...exportFilters, endDate: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">{locale === 'es' ? 'Evento (opcional)' : 'Event (optional)'}</label>
<select
value={exportFilters.eventId}
onChange={(e) => setExportFilters({ ...exportFilters, eventId: e.target.value })}
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray"
>
<option value="">{locale === 'es' ? 'Todos los Eventos' : 'All Events'}</option>
{events.map((event) => (
<option key={event.id} value={event.id}>{event.title}</option>
))}
</select>
</div>
<div className="flex gap-3 pt-4">
<Button onClick={handleExport} isLoading={exporting}>
{locale === 'es' ? 'Generar Reporte' : 'Generate Report'}
</Button>
<Button variant="outline" onClick={() => setShowExportModal(false)}>
{locale === 'es' ? 'Cancelar' : 'Cancel'}
</Button>
</div>
</div>
) : (
<div className="space-y-6">
{/* Summary */}
<div className="bg-secondary-gray rounded-btn p-4">
<h3 className="font-semibold mb-4">{locale === 'es' ? 'Resumen Financiero' : 'Financial Summary'}</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-gray-500">{locale === 'es' ? 'Total Pagado' : 'Total Paid'}</p>
<p className="text-xl font-bold text-green-600">{exportData.summary.totalPaid.toLocaleString()} PYG</p>
<p className="text-xs text-gray-500">{exportData.summary.paidCount} {locale === 'es' ? 'pagos' : 'payments'}</p>
</div>
<div>
<p className="text-gray-500">{locale === 'es' ? 'Total Pendiente' : 'Total Pending'}</p>
<p className="text-xl font-bold text-yellow-600">{exportData.summary.totalPending.toLocaleString()} PYG</p>
<p className="text-xs text-gray-500">{exportData.summary.pendingCount} {locale === 'es' ? 'pagos' : 'payments'}</p>
</div>
<div>
<p className="text-gray-500">{locale === 'es' ? 'Total Reembolsado' : 'Total Refunded'}</p>
<p className="text-xl font-bold text-blue-600">{exportData.summary.totalRefunded.toLocaleString()} PYG</p>
<p className="text-xs text-gray-500">{exportData.summary.refundedCount} {locale === 'es' ? 'pagos' : 'payments'}</p>
</div>
<div>
<p className="text-gray-500">{locale === 'es' ? 'Total Registros' : 'Total Records'}</p>
<p className="text-xl font-bold">{exportData.summary.totalPayments}</p>
</div>
</div>
</div>
{/* By Provider */}
<div className="bg-secondary-gray rounded-btn p-4">
<h3 className="font-semibold mb-4">{locale === 'es' ? 'Ingresos por Método' : 'Revenue by Method'}</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 text-sm">
<div>
<p className="text-gray-500">{locale === 'es' ? 'Efectivo' : 'Cash'}</p>
<p className="text-lg font-bold">{exportData.summary.byProvider.cash?.toLocaleString() || 0} PYG</p>
</div>
<div>
<p className="text-gray-500">Lightning</p>
<p className="text-lg font-bold">{exportData.summary.byProvider.lightning?.toLocaleString() || 0} PYG</p>
</div>
<div>
<p className="text-gray-500">{locale === 'es' ? 'Transferencia' : 'Bank Transfer'}</p>
<p className="text-lg font-bold">{(exportData.summary.byProvider as any).bank_transfer?.toLocaleString() || 0} PYG</p>
</div>
<div>
<p className="text-gray-500">TPago</p>
<p className="text-lg font-bold">{(exportData.summary.byProvider as any).tpago?.toLocaleString() || 0} PYG</p>
</div>
<div>
<p className="text-gray-500">Bancard</p>
<p className="text-lg font-bold">{exportData.summary.byProvider.bancard?.toLocaleString() || 0} PYG</p>
</div>
</div>
</div>
<div className="flex gap-3">
<Button onClick={downloadCSV}>
<ArrowDownTrayIcon className="w-4 h-4 mr-2" />
{locale === 'es' ? 'Descargar CSV' : 'Download CSV'}
</Button>
<Button variant="outline" onClick={() => setExportData(null)}>
{locale === 'es' ? 'Nuevo Reporte' : 'New Report'}
</Button>
<Button variant="outline" onClick={() => { setShowExportModal(false); setExportData(null); }}>
{locale === 'es' ? 'Cerrar' : 'Close'}
</Button>
</div>
</div>
)}
</Card>
</div>
)}
{/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<ExclamationTriangleIcon className="w-5 h-5 text-yellow-600" />
</div>
<div>
<p className="text-sm text-gray-500">{locale === 'es' ? 'Pendientes de Aprobación' : 'Pending Approval'}</p>
<p className="text-xl font-bold text-yellow-600">{pendingApprovalBookingsCount}</p>
{pendingApprovalPayments.length !== pendingApprovalBookingsCount && (
<p className="text-xs text-gray-400">({pendingApprovalPayments.length} tickets)</p>
)}
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
<ClockIcon className="w-5 h-5 text-gray-600" />
</div>
<div>
<p className="text-sm text-gray-500">{locale === 'es' ? 'Total Pendiente' : 'Total Pending'}</p>
<p className="text-xl font-bold">{formatCurrency(totalPending, 'PYG')}</p>
<p className="text-xs text-gray-400">{pendingBookingsCount} {locale === 'es' ? 'reservas' : 'bookings'}</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircleIcon className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-sm text-gray-500">{locale === 'es' ? 'Total Pagado' : 'Total Paid'}</p>
<p className="text-xl font-bold text-green-600">{formatCurrency(totalPaid, 'PYG')}</p>
<p className="text-xs text-gray-400">{paidBookingsCount} {locale === 'es' ? 'reservas' : 'bookings'}</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<BoltIcon className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-sm text-gray-500">{locale === 'es' ? 'Total Tickets' : 'Total Tickets'}</p>
<p className="text-xl font-bold">{payments.length}</p>
</div>
</div>
</Card>
</div>
{/* Tabs */}
<div className="border-b mb-6">
<nav className="flex gap-4">
<button
onClick={() => setActiveTab('pending_approval')}
className={`pb-3 px-1 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'pending_approval'
? 'border-primary-yellow text-primary-dark'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{locale === 'es' ? 'Pendientes de Aprobación' : 'Pending Approval'}
{pendingApprovalPayments.length > 0 && (
<span className="ml-2 bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded-full text-xs">
{pendingApprovalPayments.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab('all')}
className={`pb-3 px-1 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'all'
? 'border-primary-yellow text-primary-dark'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{locale === 'es' ? 'Todos los Pagos' : 'All Payments'}
</button>
</nav>
</div>
{/* Pending Approval Tab */}
{activeTab === 'pending_approval' && (
<>
{pendingApprovalPayments.length === 0 ? (
<Card className="p-12 text-center">
<CheckCircleIcon className="w-12 h-12 text-green-400 mx-auto mb-4" />
<p className="text-gray-500">
{locale === 'es'
? 'No hay pagos pendientes de aprobación'
: 'No payments pending approval'}
</p>
</Card>
) : (
<div className="space-y-4">
{pendingApprovalPayments.map((payment) => {
const bookingInfo = getPendingBookingInfo(payment);
return (
<Card key={payment.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center flex-shrink-0">
{getProviderIcon(payment.provider)}
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<p className="font-bold text-lg">{formatCurrency(bookingInfo.bookingTotal, payment.currency)}</p>
{bookingInfo.ticketCount > 1 && (
<span className="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full">
📦 {bookingInfo.ticketCount} tickets × {formatCurrency(payment.amount, payment.currency)}
</span>
)}
{getStatusBadge(payment.status)}
</div>
{payment.ticket && (
<p className="text-sm font-medium">
{payment.ticket.attendeeFirstName} {payment.ticket.attendeeLastName}
{bookingInfo.ticketCount > 1 && <span className="text-gray-400 font-normal"> +{bookingInfo.ticketCount - 1} {locale === 'es' ? 'más' : 'more'}</span>}
</p>
)}
{payment.event && (
<p className="text-sm text-gray-500">{payment.event.title}</p>
)}
<div className="flex items-center gap-4 mt-2 text-xs text-gray-400">
<span className="flex items-center gap-1">
{getProviderIcon(payment.provider)}
{getProviderLabel(payment.provider)}
</span>
{payment.userMarkedPaidAt && (
<span className="flex items-center gap-1">
<ClockIcon className="w-3 h-3" />
{locale === 'es' ? 'Marcado:' : 'Marked:'} {formatDate(payment.userMarkedPaidAt)}
</span>
)}
</div>
{payment.payerName && (
<p className="text-xs text-amber-600 mt-1 font-medium">
{locale === 'es' ? 'Pago por:' : 'Paid by:'} {payment.payerName}
</p>
)}
</div>
</div>
<Button onClick={() => setSelectedPayment(payment)}>
{locale === 'es' ? 'Revisar' : 'Review'}
</Button>
</div>
</Card>
);
})}
</div>
)}
</>
)}
{/* All Payments Tab */}
{activeTab === 'all' && (
<>
{/* Filters */}
<Card className="p-4 mb-6">
<div className="flex flex-wrap gap-4">
<div>
<label className="block text-sm font-medium mb-1">Status</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-4 py-2 rounded-btn border border-secondary-light-gray min-w-[150px]"
>
<option value="">{locale === 'es' ? 'Todos los Estados' : 'All Statuses'}</option>
<option value="pending">{locale === 'es' ? 'Pendiente' : 'Pending'}</option>
<option value="pending_approval">{locale === 'es' ? 'Esperando Aprobación' : 'Pending Approval'}</option>
<option value="paid">{locale === 'es' ? 'Pagado' : 'Paid'}</option>
<option value="refunded">{locale === 'es' ? 'Reembolsado' : 'Refunded'}</option>
<option value="failed">{locale === 'es' ? 'Fallido' : 'Failed'}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">{locale === 'es' ? 'Método' : 'Provider'}</label>
<select
value={providerFilter}
onChange={(e) => setProviderFilter(e.target.value)}
className="px-4 py-2 rounded-btn border border-secondary-light-gray min-w-[150px]"
>
<option value="">{locale === 'es' ? 'Todos los Métodos' : 'All Providers'}</option>
<option value="lightning">Lightning</option>
<option value="cash">{locale === 'es' ? 'Efectivo' : 'Cash'}</option>
<option value="bank_transfer">{locale === 'es' ? 'Transferencia Bancaria' : 'Bank Transfer'}</option>
<option value="tpago">TPago</option>
</select>
</div>
</div>
</Card>
{/* Payments Table */}
<Card className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-secondary-gray">
<tr>
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">{locale === 'es' ? 'Asistente' : 'Attendee'}</th>
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">{locale === 'es' ? 'Evento' : 'Event'}</th>
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">{locale === 'es' ? 'Monto' : 'Amount'}</th>
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">{locale === 'es' ? 'Método' : 'Method'}</th>
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">{locale === 'es' ? 'Fecha' : 'Date'}</th>
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Status</th>
<th className="text-right px-6 py-3 text-sm font-medium text-gray-600">{locale === 'es' ? 'Acciones' : 'Actions'}</th>
</tr>
</thead>
<tbody className="divide-y divide-secondary-light-gray">
{payments.length === 0 ? (
<tr>
<td colSpan={7} className="px-6 py-12 text-center text-gray-500">
{locale === 'es' ? 'No se encontraron pagos' : 'No payments found'}
</td>
</tr>
) : (
payments.map((payment) => {
const bookingInfo = getBookingInfo(payment);
return (
<tr key={payment.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
{payment.ticket ? (
<div>
<p className="font-medium text-sm">
{payment.ticket.attendeeFirstName} {payment.ticket.attendeeLastName}
</p>
<p className="text-xs text-gray-500">{payment.ticket.attendeeEmail}</p>
{payment.payerName && (
<p className="text-xs text-amber-600 mt-1">
{locale === 'es' ? 'Pagado por:' : 'Paid by:'} {payment.payerName}
</p>
)}
</div>
) : (
<span className="text-gray-400 text-sm">-</span>
)}
</td>
<td className="px-6 py-4">
{payment.event ? (
<p className="text-sm">{payment.event.title}</p>
) : (
<span className="text-gray-400 text-sm">-</span>
)}
</td>
<td className="px-6 py-4">
<div>
<p className="font-medium">{formatCurrency(bookingInfo.bookingTotal, payment.currency)}</p>
{bookingInfo.ticketCount > 1 && (
<p className="text-xs text-purple-600 mt-1">
📦 {bookingInfo.ticketCount} × {formatCurrency(payment.amount, payment.currency)}
</p>
)}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2 text-sm text-gray-600">
{getProviderIcon(payment.provider)}
{getProviderLabel(payment.provider)}
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-600">
{formatDate(payment.createdAt)}
</td>
<td className="px-6 py-4">
<div className="space-y-1">
{getStatusBadge(payment.status)}
{payment.ticket?.bookingId && (
<p className="text-xs text-purple-600" title="Part of multi-ticket booking">
📦 {locale === 'es' ? 'Grupo' : 'Group'}
</p>
)}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center justify-end gap-2">
{(payment.status === 'pending' || payment.status === 'pending_approval') && (
<Button
size="sm"
onClick={() => setSelectedPayment(payment)}
>
<CheckCircleIcon className="w-4 h-4 mr-1" />
{locale === 'es' ? 'Revisar' : 'Review'}
</Button>
)}
{payment.status === 'paid' && (
<Button
size="sm"
variant="outline"
onClick={() => handleRefund(payment.id)}
>
<ArrowPathIcon className="w-4 h-4 mr-1" />
{t('admin.payments.refund')}
</Button>
)}
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</Card>
</>
)}
</div>
);
}