From 25b701874374c78197ac249dfe46b8a416fce2cd Mon Sep 17 00:00:00 2001 From: Michilis Date: Sat, 7 Mar 2026 18:06:35 +0000 Subject: [PATCH] Bookings/payments/linktree: fix payment method display, event filter, logo, search - Bookings: align payment method labels with payments page (bank_transfer, tpago, etc), add sibling fallback - Payments: add event filter (single/multi), add search by name/email/event - Linktree: use Spanglish logo instead of icon - API: payments getAll supports eventId/eventIds Made-with: Cursor --- backend/src/routes/payments.ts | 14 ++- frontend/src/app/admin/bookings/page.tsx | 23 +++- frontend/src/app/admin/payments/page.tsx | 143 +++++++++++++++++++---- frontend/src/app/linktree/page.tsx | 6 +- frontend/src/lib/api.ts | 4 +- 5 files changed, 159 insertions(+), 31 deletions(-) diff --git a/backend/src/routes/payments.ts b/backend/src/routes/payments.ts index ca8dbd4..cd60ebe 100644 --- a/backend/src/routes/payments.ts +++ b/backend/src/routes/payments.ts @@ -30,6 +30,8 @@ paymentsRouter.get('/', requireAuth(['admin']), async (c) => { const status = c.req.query('status'); const provider = c.req.query('provider'); const pendingApproval = c.req.query('pendingApproval'); + const eventId = c.req.query('eventId'); + const eventIds = c.req.query('eventIds'); // Get all payments with their associated tickets let allPayments = await dbAll( @@ -55,7 +57,7 @@ paymentsRouter.get('/', requireAuth(['admin']), async (c) => { } // Enrich with ticket and event data - const enrichedPayments = await Promise.all( + let enrichedPayments = await Promise.all( allPayments.map(async (payment: any) => { const ticket = await dbGet( (db as any) @@ -94,6 +96,16 @@ paymentsRouter.get('/', requireAuth(['admin']), async (c) => { }) ); + // Filter by event(s) + if (eventId) { + enrichedPayments = enrichedPayments.filter((p: any) => p.event?.id === eventId); + } else if (eventIds) { + const ids = eventIds.split(',').map((s: string) => s.trim()).filter(Boolean); + if (ids.length > 0) { + enrichedPayments = enrichedPayments.filter((p: any) => p.event && ids.includes(p.event.id)); + } + } + return c.json({ payments: enrichedPayments }); }); diff --git a/frontend/src/app/admin/bookings/page.tsx b/frontend/src/app/admin/bookings/page.tsx index 161727c..cb5fdc7 100644 --- a/frontend/src/app/admin/bookings/page.tsx +++ b/frontend/src/app/admin/bookings/page.tsx @@ -154,12 +154,23 @@ export default function AdminBookingsPage() { }; const getPaymentMethodLabel = (provider: string) => { - switch (provider) { - case 'bancard': return 'TPago / Card'; - case 'lightning': return 'Bitcoin Lightning'; - case 'cash': return 'Cash at Event'; - default: return provider; + const labels: Record = { + cash: locale === 'es' ? 'Efectivo en el Evento' : 'Cash at Event', + bank_transfer: locale === 'es' ? 'Transferencia Bancaria' : 'Bank Transfer', + lightning: 'Lightning', + tpago: 'TPago', + bancard: 'Bancard', + }; + return labels[provider] || provider; + }; + + const getDisplayProvider = (ticket: TicketWithDetails) => { + if (ticket.payment?.provider) return ticket.payment.provider; + if (ticket.bookingId) { + const sibling = tickets.find(t => t.bookingId === ticket.bookingId && t.payment?.provider); + return sibling?.payment?.provider ?? 'cash'; } + return 'cash'; }; const filteredTickets = tickets.filter((ticket) => { @@ -394,7 +405,7 @@ export default function AdminBookingsPage() { {ticket.payment?.status || 'pending'} -

{getPaymentMethodLabel(ticket.payment?.provider || 'cash')}

+

{getPaymentMethodLabel(getDisplayProvider(ticket))}

{ticket.payment && (

{bookingInfo.bookingTotal.toLocaleString()} {ticket.payment.currency}

)} diff --git a/frontend/src/app/admin/payments/page.tsx b/frontend/src/app/admin/payments/page.tsx index 007669d..c90b650 100644 --- a/frontend/src/app/admin/payments/page.tsx +++ b/frontend/src/app/admin/payments/page.tsx @@ -22,6 +22,7 @@ import { CreditCardIcon, EnvelopeIcon, FunnelIcon, + MagnifyingGlassIcon, XMarkIcon, } from '@heroicons/react/24/outline'; import toast from 'react-hot-toast'; @@ -38,6 +39,8 @@ export default function AdminPaymentsPage() { const [activeTab, setActiveTab] = useState('pending_approval'); const [statusFilter, setStatusFilter] = useState(''); const [providerFilter, setProviderFilter] = useState(''); + const [eventFilter, setEventFilter] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); const [mobileFilterOpen, setMobileFilterOpen] = useState(false); // Modal state @@ -59,7 +62,7 @@ export default function AdminPaymentsPage() { useEffect(() => { loadData(); - }, [statusFilter, providerFilter]); + }, [statusFilter, providerFilter, eventFilter]); const loadData = async () => { try { @@ -68,7 +71,8 @@ export default function AdminPaymentsPage() { paymentsApi.getPendingApproval(), paymentsApi.getAll({ status: statusFilter || undefined, - provider: providerFilter || undefined + provider: providerFilter || undefined, + eventIds: eventFilter.length > 0 ? eventFilter : undefined, }), eventsApi.getAll(), ]); @@ -751,11 +755,40 @@ export default function AdminPaymentsPage() { )} {/* All Payments Tab */} - {activeTab === 'all' && ( + {activeTab === 'all' && (() => { + const q = searchQuery.trim().toLowerCase(); + const filteredPayments = q + ? payments.filter((p) => { + const name = `${p.ticket?.attendeeFirstName || ''} ${p.ticket?.attendeeLastName || ''}`.trim().toLowerCase(); + const email = (p.ticket?.attendeeEmail || '').toLowerCase(); + const phone = (p.ticket?.attendeePhone || '').toLowerCase(); + const eventTitle = (p.event?.title || '').toLowerCase(); + const payerName = (p.payerName || '').toLowerCase(); + const reference = (p.reference || '').toLowerCase(); + const id = (p.id || '').toLowerCase(); + return name.includes(q) || email.includes(q) || phone.includes(q) || + eventTitle.includes(q) || payerName.includes(q) || reference.includes(q) || id.includes(q); + }) + : payments; + + return ( <> {/* Desktop Filters */}
+
+ +
+ + setSearchQuery(e.target.value)} + className="w-full pl-9 pr-3 py-2 rounded-btn border border-secondary-light-gray text-sm focus:outline-none focus:ring-2 focus:ring-primary-yellow min-w-[200px]" + /> +
+
+
+ + + {eventFilter.length > 0 && ( +
+ {eventFilter.map((id) => { + const ev = events.find(e => e.id === id); + return ( + + {ev?.title || id} + + + ); + })} + +
+ )} +
- {/* Mobile Filter Toolbar */} -
- - {(statusFilter || providerFilter) && ( - - )} + {/* Mobile Search & Filter Toolbar */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-9 pr-3 py-2.5 rounded-btn border border-secondary-light-gray text-sm focus:outline-none focus:ring-2 focus:ring-primary-yellow" + /> +
+
+ + {(statusFilter || providerFilter || eventFilter.length > 0 || searchQuery) && ( + + )} +
{/* Desktop: Table */} @@ -810,10 +888,10 @@ export default function AdminPaymentsPage() { - {payments.length === 0 ? ( + {filteredPayments.length === 0 ? ( {locale === 'es' ? 'No se encontraron pagos' : 'No payments found'} ) : ( - payments.map((payment) => { + filteredPayments.map((payment) => { const bookingInfo = getBookingInfo(payment); return ( @@ -858,13 +936,18 @@ export default function AdminPaymentsPage() {
+ {(searchQuery || filteredPayments.length !== payments.length) && ( +

+ {locale === 'es' ? 'Mostrando' : 'Showing'} {filteredPayments.length} {locale === 'es' ? 'de' : 'of'} {payments.length} +

+ )} {/* Mobile: Card List */}
- {payments.length === 0 ? ( + {filteredPayments.length === 0 ? (
{locale === 'es' ? 'No se encontraron pagos' : 'No payments found'}
) : ( - payments.map((payment) => { + filteredPayments.map((payment) => { const bookingInfo = getBookingInfo(payment); return ( @@ -911,6 +994,25 @@ export default function AdminPaymentsPage() { {/* Mobile Filter BottomSheet */} setMobileFilterOpen(false)} title={locale === 'es' ? 'Filtros' : 'Filters'}>
+
+ +
+ {events.map((event) => ( + + ))} +
+
- +
- )} + ); + })()}
diff --git a/frontend/src/app/linktree/page.tsx b/frontend/src/app/linktree/page.tsx index 85dddf8..47bd346 100644 --- a/frontend/src/app/linktree/page.tsx +++ b/frontend/src/app/linktree/page.tsx @@ -2,13 +2,13 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; +import Image from 'next/image'; import { useLanguage } from '@/context/LanguageContext'; import { eventsApi, Event } from '@/lib/api'; import { formatPrice, formatDateShort, formatTime } from '@/lib/utils'; import { CalendarIcon, MapPinIcon, - ChatBubbleLeftRightIcon, } from '@heroicons/react/24/outline'; export default function LinktreePage() { @@ -59,8 +59,8 @@ export default function LinktreePage() {
{/* Profile Header */}
-
- +
+ Spanglish

Spanglish

{t('linktree.tagline')}

diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 22614b5..c59f3c2 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -236,11 +236,13 @@ export const usersApi = { // Payments API export const paymentsApi = { - getAll: (params?: { status?: string; provider?: string; pendingApproval?: boolean }) => { + getAll: (params?: { status?: string; provider?: string; pendingApproval?: boolean; eventId?: string; eventIds?: string[] }) => { const query = new URLSearchParams(); if (params?.status) query.set('status', params.status); if (params?.provider) query.set('provider', params.provider); if (params?.pendingApproval) query.set('pendingApproval', 'true'); + if (params?.eventId) query.set('eventId', params.eventId); + if (params?.eventIds && params.eventIds.length > 0) query.set('eventIds', params.eventIds.join(',')); return fetchApi<{ payments: PaymentWithDetails[] }>(`/api/payments?${query}`); },