Mobile-friendly admin pages, redesigned homepage Next Event card
- Extract shared mobile components (BottomSheet, MoreMenu, Dropdown, etc.) into MobileComponents.tsx - Make admin pages mobile-friendly: bookings, emails, events, faq, payments, tickets, users - Redesign homepage Next Event card with banner image, responsive layout, and updated styling - Fix past events showing on homepage/linktree: use proper Date comparison, auto-unfeature expired events - Add "Over" tag to admin events list for past events - Fix backend FRONTEND_URL for cache revalidation Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { paymentsApi, adminApi, eventsApi, PaymentWithDetails, Event, ExportedPa
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import { BottomSheet, MoreMenu, DropdownItem, AdminMobileStyles } from '@/components/admin/MobileComponents';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ArrowPathIcon,
|
||||
@@ -20,8 +21,11 @@ import {
|
||||
BuildingLibraryIcon,
|
||||
CreditCardIcon,
|
||||
EnvelopeIcon,
|
||||
FunnelIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import toast from 'react-hot-toast';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type Tab = 'pending_approval' | 'all';
|
||||
|
||||
@@ -34,6 +38,7 @@ export default function AdminPaymentsPage() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('pending_approval');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
const [providerFilter, setProviderFilter] = useState<string>('');
|
||||
const [mobileFilterOpen, setMobileFilterOpen] = useState(false);
|
||||
|
||||
// Modal state
|
||||
const [selectedPayment, setSelectedPayment] = useState<PaymentWithDetails | null>(null);
|
||||
@@ -329,10 +334,11 @@ export default function AdminPaymentsPage() {
|
||||
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'}
|
||||
<h1 className="text-xl md:text-2xl font-bold text-primary-dark">{t('admin.payments.title')}</h1>
|
||||
<Button onClick={() => setShowExportModal(true)} size="sm" className="min-h-[44px] md:min-h-0">
|
||||
<DocumentArrowDownIcon className="w-4 h-4 mr-1.5" />
|
||||
<span className="hidden md:inline">{locale === 'es' ? 'Exportar Datos' : 'Export Data'}</span>
|
||||
<span className="md:hidden">{locale === 'es' ? 'Exportar' : 'Export'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -340,11 +346,18 @@ export default function AdminPaymentsPage() {
|
||||
{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="fixed inset-0 bg-black/50 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
|
||||
<Card className="w-full md:max-w-lg max-h-[90vh] flex flex-col overflow-hidden rounded-t-2xl md:rounded-card">
|
||||
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray flex-shrink-0">
|
||||
<h2 className="text-base font-bold">
|
||||
{locale === 'es' ? 'Verificar Pago' : 'Verify Payment'}
|
||||
</h2>
|
||||
<button onClick={() => { setSelectedPayment(null); setNoteText(''); setSendEmail(true); }}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 overflow-y-auto flex-1 min-h-0">
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
@@ -442,43 +455,24 @@ export default function AdminPaymentsPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={() => handleApprove(selectedPayment)}
|
||||
isLoading={processing}
|
||||
className="flex-1"
|
||||
>
|
||||
<Button onClick={() => handleApprove(selectedPayment)} isLoading={processing} className="flex-1 min-h-[44px]">
|
||||
<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"
|
||||
>
|
||||
<Button variant="outline" onClick={() => handleReject(selectedPayment)} isLoading={processing}
|
||||
className="flex-1 border-red-300 text-red-600 hover:bg-red-50 min-h-[44px]">
|
||||
<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"
|
||||
>
|
||||
<Button variant="outline" onClick={() => handleSendReminder(selectedPayment)} isLoading={sendingReminder} className="w-full min-h-[44px]">
|
||||
<EnvelopeIcon className="w-5 h-5 mr-2" />
|
||||
{locale === 'es' ? 'Enviar recordatorio de pago' : 'Send payment reminder'}
|
||||
{locale === 'es' ? 'Enviar recordatorio' : 'Send 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>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
@@ -486,9 +480,16 @@ export default function AdminPaymentsPage() {
|
||||
|
||||
{/* 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>
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
|
||||
<Card className="w-full md:max-w-2xl max-h-[90vh] flex flex-col overflow-hidden rounded-t-2xl md:rounded-card">
|
||||
<div className="flex items-center justify-between p-4 border-b border-secondary-light-gray flex-shrink-0">
|
||||
<h2 className="text-base font-bold">{locale === 'es' ? 'Exportar Datos Financieros' : 'Export Financial Data'}</h2>
|
||||
<button onClick={() => { setShowExportModal(false); setExportData(null); }}
|
||||
className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 overflow-y-auto flex-1 min-h-0">
|
||||
|
||||
{!exportData ? (
|
||||
<div className="space-y-4">
|
||||
@@ -522,10 +523,10 @@ export default function AdminPaymentsPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button onClick={handleExport} isLoading={exporting}>
|
||||
<Button onClick={handleExport} isLoading={exporting} className="flex-1 min-h-[44px]">
|
||||
{locale === 'es' ? 'Generar Reporte' : 'Generate Report'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setShowExportModal(false)}>
|
||||
<Button variant="outline" onClick={() => setShowExportModal(false)} className="flex-1 min-h-[44px]">
|
||||
{locale === 'es' ? 'Cancelar' : 'Cancel'}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -585,20 +586,21 @@ export default function AdminPaymentsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={downloadCSV}>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button onClick={downloadCSV} className="min-h-[44px]">
|
||||
<ArrowDownTrayIcon className="w-4 h-4 mr-2" />
|
||||
{locale === 'es' ? 'Descargar CSV' : 'Download CSV'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setExportData(null)}>
|
||||
<Button variant="outline" onClick={() => setExportData(null)} className="min-h-[44px]">
|
||||
{locale === 'es' ? 'Nuevo Reporte' : 'New Report'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => { setShowExportModal(false); setExportData(null); }}>
|
||||
<Button variant="outline" onClick={() => { setShowExportModal(false); setExportData(null); }} className="min-h-[44px]">
|
||||
{locale === 'es' ? 'Cerrar' : 'Close'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
@@ -657,31 +659,19 @@ export default function AdminPaymentsPage() {
|
||||
</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'}
|
||||
<div className="border-b mb-6 overflow-x-auto scrollbar-hide">
|
||||
<nav className="flex gap-4 min-w-max">
|
||||
<button onClick={() => setActiveTab('pending_approval')}
|
||||
className={clsx('pb-3 px-1 text-sm font-medium border-b-2 transition-colors whitespace-nowrap min-h-[44px]',
|
||||
activeTab === 'pending_approval' ? 'border-primary-yellow text-primary-dark' : 'border-transparent text-gray-500 hover:text-gray-700')}>
|
||||
{locale === 'es' ? 'Pendientes' : '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>
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
<button onClick={() => setActiveTab('all')}
|
||||
className={clsx('pb-3 px-1 text-sm font-medium border-b-2 transition-colors whitespace-nowrap min-h-[44px]',
|
||||
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>
|
||||
@@ -748,7 +738,7 @@ export default function AdminPaymentsPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => setSelectedPayment(payment)}>
|
||||
<Button onClick={() => setSelectedPayment(payment)} size="sm" className="min-h-[44px] md:min-h-0 flex-shrink-0">
|
||||
{locale === 'es' ? 'Revisar' : 'Review'}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -763,16 +753,13 @@ export default function AdminPaymentsPage() {
|
||||
{/* All Payments Tab */}
|
||||
{activeTab === 'all' && (
|
||||
<>
|
||||
{/* Filters */}
|
||||
<Card className="p-4 mb-6">
|
||||
{/* Desktop Filters */}
|
||||
<Card className="p-4 mb-6 hidden md:block">
|
||||
<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]"
|
||||
>
|
||||
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 rounded-btn border border-secondary-light-gray min-w-[150px] text-sm">
|
||||
<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>
|
||||
@@ -783,119 +770,81 @@ export default function AdminPaymentsPage() {
|
||||
</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]"
|
||||
>
|
||||
<select value={providerFilter} onChange={(e) => setProviderFilter(e.target.value)}
|
||||
className="px-4 py-2 rounded-btn border border-secondary-light-gray min-w-[150px] text-sm">
|
||||
<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="bank_transfer">{locale === 'es' ? 'Transferencia' : 'Bank Transfer'}</option>
|
||||
<option value="tpago">TPago</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Payments Table */}
|
||||
<Card className="overflow-hidden">
|
||||
{/* Mobile Filter Toolbar */}
|
||||
<div className="md:hidden mb-4 flex items-center gap-2">
|
||||
<button onClick={() => setMobileFilterOpen(true)}
|
||||
className={clsx('flex items-center gap-1.5 px-3 py-2 rounded-btn border text-sm min-h-[44px]',
|
||||
(statusFilter || providerFilter) ? 'border-primary-yellow bg-yellow-50 text-primary-dark' : 'border-secondary-light-gray text-gray-600')}>
|
||||
<FunnelIcon className="w-4 h-4" /> Filters
|
||||
</button>
|
||||
{(statusFilter || providerFilter) && (
|
||||
<button onClick={() => { setStatusFilter(''); setProviderFilter(''); }}
|
||||
className="text-xs text-primary-yellow min-h-[44px] flex items-center">Clear</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop: Table */}
|
||||
<Card className="overflow-hidden hidden md:block">
|
||||
<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>
|
||||
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">{locale === 'es' ? 'Asistente' : 'Attendee'}</th>
|
||||
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">{locale === 'es' ? 'Evento' : 'Event'}</th>
|
||||
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">{locale === 'es' ? 'Monto' : 'Amount'}</th>
|
||||
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">{locale === 'es' ? 'Método' : 'Method'}</th>
|
||||
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="text-right px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">{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>
|
||||
<tr><td colSpan={6} className="px-4 py-12 text-center text-gray-500 text-sm">{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">
|
||||
<td className="px-4 py-3">
|
||||
{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>
|
||||
)}
|
||||
<p className="font-medium text-sm">{payment.ticket.attendeeFirstName} {payment.ticket.attendeeLastName}</p>
|
||||
<p className="text-xs text-gray-500 truncate max-w-[180px]">{payment.ticket.attendeeEmail}</p>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400 text-sm">-</span>
|
||||
)}
|
||||
) : <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 className="px-4 py-3 text-sm truncate max-w-[150px]">{payment.event?.title || '-'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<p className="font-medium text-sm">{formatCurrency(bookingInfo.bookingTotal, payment.currency)}</p>
|
||||
{bookingInfo.ticketCount > 1 && <p className="text-[10px] text-purple-600">{bookingInfo.ticketCount} tickets</p>}
|
||||
</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>
|
||||
)}
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-600">
|
||||
{getProviderIcon(payment.provider)} {getProviderLabel(payment.provider)}
|
||||
</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">
|
||||
<td className="px-4 py-3">{getStatusBadge(payment.status)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{(payment.status === 'pending' || payment.status === 'pending_approval') && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setSelectedPayment(payment)}
|
||||
>
|
||||
<CheckCircleIcon className="w-4 h-4 mr-1" />
|
||||
<Button size="sm" onClick={() => setSelectedPayment(payment)} className="text-xs px-2 py-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" />
|
||||
<Button size="sm" variant="outline" onClick={() => handleRefund(payment.id)} className="text-xs px-2 py-1">
|
||||
{t('admin.payments.refund')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -909,8 +858,92 @@ export default function AdminPaymentsPage() {
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Mobile: Card List */}
|
||||
<div className="md:hidden space-y-2">
|
||||
{payments.length === 0 ? (
|
||||
<div className="text-center py-10 text-gray-500 text-sm">{locale === 'es' ? 'No se encontraron pagos' : 'No payments found'}</div>
|
||||
) : (
|
||||
payments.map((payment) => {
|
||||
const bookingInfo = getBookingInfo(payment);
|
||||
return (
|
||||
<Card key={payment.id} className="p-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
{payment.ticket ? (
|
||||
<p className="font-medium text-sm truncate">{payment.ticket.attendeeFirstName} {payment.ticket.attendeeLastName}</p>
|
||||
) : <p className="text-sm text-gray-400">-</p>}
|
||||
<p className="text-xs text-gray-500 truncate">{payment.event?.title || '-'}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
{getStatusBadge(payment.status)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2 text-xs text-gray-500">
|
||||
<span className="font-medium text-gray-700">{formatCurrency(bookingInfo.bookingTotal, payment.currency)}</span>
|
||||
<span className="text-gray-300">|</span>
|
||||
<span className="flex items-center gap-1">{getProviderIcon(payment.provider)} {getProviderLabel(payment.provider)}</span>
|
||||
{bookingInfo.ticketCount > 1 && (
|
||||
<><span className="text-gray-300">|</span><span className="text-purple-600">{bookingInfo.ticketCount} tickets</span></>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2 pt-2 border-t border-gray-100">
|
||||
<p className="text-[10px] text-gray-400">{formatDate(payment.createdAt)}</p>
|
||||
<div className="flex items-center gap-1">
|
||||
{(payment.status === 'pending' || payment.status === 'pending_approval') && (
|
||||
<Button size="sm" onClick={() => setSelectedPayment(payment)} className="text-xs px-2.5 py-1.5 min-h-[36px]">
|
||||
{locale === 'es' ? 'Revisar' : 'Review'}
|
||||
</Button>
|
||||
)}
|
||||
{payment.status === 'paid' && (
|
||||
<Button size="sm" variant="outline" onClick={() => handleRefund(payment.id)} className="text-xs px-2.5 py-1.5 min-h-[36px]">
|
||||
{t('admin.payments.refund')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Filter BottomSheet */}
|
||||
<BottomSheet open={mobileFilterOpen} onClose={() => setMobileFilterOpen(false)} title={locale === 'es' ? 'Filtros' : 'Filters'}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
||||
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="w-full px-3 py-2.5 rounded-btn border border-secondary-light-gray text-sm min-h-[44px]">
|
||||
<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 text-gray-700 mb-1">{locale === 'es' ? 'Método' : 'Provider'}</label>
|
||||
<select value={providerFilter} onChange={(e) => setProviderFilter(e.target.value)}
|
||||
className="w-full px-3 py-2.5 rounded-btn border border-secondary-light-gray text-sm min-h-[44px]">
|
||||
<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' : 'Bank Transfer'}</option>
|
||||
<option value="tpago">TPago</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button variant="outline" onClick={() => { setStatusFilter(''); setProviderFilter(''); setMobileFilterOpen(false); }} className="flex-1 min-h-[44px]">Clear</Button>
|
||||
<Button onClick={() => setMobileFilterOpen(false)} className="flex-1 min-h-[44px]">Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
</BottomSheet>
|
||||
</>
|
||||
)}
|
||||
|
||||
<AdminMobileStyles />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user