22 Commits

Author SHA1 Message Date
defd9685e0 Merge pull request 'dev' (#18) from dev into main
Reviewed-on: #18
2026-04-27 20:42:36 +00:00
Michilis
22e9254f42 fix(tickets): use integer 1 for isGuest in admin guest invite
Postgres is_guest column is integer; passing JS boolean true caused
"invalid input syntax for type integer" 500 on POST /api/tickets/admin/guest.

Made-with: Cursor
2026-04-27 20:41:58 +00:00
Michilis
2cabd8c92f fix(admin): avoid rendering stray 0 for non-guest tickets on isGuest
Made-with: Love
2026-04-27 18:21:10 +00:00
Michilis
622bb5171c fix(admin): hide pending-approval payments for past events
Made-with: Cursor
2026-04-27 18:14:15 +00:00
Michilis
55516ef1e7 chore: ignore .agents directory and skills-lock.json
Made-with: Cursor
2026-04-27 18:14:15 +00:00
1ed62b0d3f Merge pull request 'Fix db:export ENOBUFS by streaming pg_dump output to file' (#17) from dev into main
Reviewed-on: #17
2026-03-12 19:18:57 +00:00
91de6df04d Merge pull request 'feat(emails): add re-send for all emails, failed tab, and resend indicators' (#16) from dev into main
Reviewed-on: #16
2026-03-12 19:14:36 +00:00
a5d97d65e1 Merge pull request 'Admin: stats privacy toggle, clickable event rows, fix payment method display' (#15) from dev into main
Reviewed-on: #15
2026-03-10 01:14:36 +00:00
f0128f66b0 Merge pull request 'Bug fixes and improvements' (#14) from dev into main
Reviewed-on: #14
2026-03-07 22:53:13 +00:00
b33c68feb0 Merge pull request 'dev' (#13) from dev into main
Reviewed-on: #13
2026-02-19 02:23:19 +00:00
15655e3987 Merge pull request 'dev' (#12) from dev into main
Reviewed-on: #12
2026-02-16 23:11:52 +00:00
d8b3864411 Merge pull request 'Fix stale featured event on homepage: revalidate cache when featured event changes' (#11) from dev into main
Reviewed-on: #11
2026-02-16 22:44:19 +00:00
194cbd6ca8 Merge pull request 'Scanner: close button on valid ticket, camera lifecycle fix' (#10) from dev into main
Reviewed-on: #10
2026-02-14 19:04:42 +00:00
d5445c2282 Merge pull request 'Admin event page: redesign UI, export endpoints, mobile fixes' (#9) from dev into main
Reviewed-on: #9
2026-02-14 18:38:57 +00:00
dcfefc8371 Merge pull request 'feat(admin): add event attendees export (CSV) with status filters' (#8) from dev into main
Reviewed-on: #8
2026-02-14 05:28:24 +00:00
b5f14335c4 Merge pull request 'Mobile scanner redesign + backend live search' (#7) from dev into main
Reviewed-on: #7
2026-02-14 04:28:44 +00:00
d44ac949b5 Merge pull request 'Email queue + async sending; legal settings and placeholders' (#6) from dev into main
Reviewed-on: #6
2026-02-12 21:04:58 +00:00
a5e939221d Merge pull request 'dev' (#5) from dev into main
Reviewed-on: #5
2026-02-12 07:56:37 +00:00
833e3e5a9c Merge pull request 'Fix llms.txt event times: format in America/Asuncion timezone' (#4) from dev into main
Reviewed-on: #4
2026-02-12 06:28:51 +00:00
ba1975dd6d Merge pull request 'dev' (#3) from dev into main
Reviewed-on: #3
2026-02-12 04:55:39 +00:00
3025ef3d21 Merge pull request 'dev' (#2) from dev into main
Reviewed-on: #2
2026-02-12 03:19:06 +00:00
8564f8af83 Merge pull request 'dev' (#1) from dev into main
Reviewed-on: #1
2026-02-12 02:18:08 +00:00
4 changed files with 29 additions and 11 deletions

2
.gitignore vendored
View File

@@ -37,6 +37,8 @@ backend/uploads/
# Tooling # Tooling
.turbo/ .turbo/
.cursor/ .cursor/
.agents/
skills-lock.json
.npm-cache/ .npm-cache/
# OS # OS

View File

@@ -1476,7 +1476,7 @@ ticketsRouter.post('/admin/guest', requireAuth(['admin', 'organizer', 'staff']),
attendeePhone: data.phone && data.phone.trim() ? data.phone.trim() : null, attendeePhone: data.phone && data.phone.trim() ? data.phone.trim() : null,
preferredLanguage: data.preferredLanguage || null, preferredLanguage: data.preferredLanguage || null,
status: 'confirmed', status: 'confirmed',
isGuest: true, isGuest: 1,
qrCode, qrCode,
checkinAt: null, checkinAt: null,
adminNote: data.adminNote || null, adminNote: data.adminNote || null,

View File

@@ -1024,7 +1024,7 @@ export default function AdminEventDetailPage() {
<td className="px-4 py-2.5"> <td className="px-4 py-2.5">
<div className="flex items-center gap-1 flex-wrap"> <div className="flex items-center gap-1 flex-wrap">
{getStatusBadge(ticket.status, true)} {getStatusBadge(ticket.status, true)}
{ticket.isGuest && ( {!!ticket.isGuest && (
<span className="px-1.5 py-0.5 text-[10px] rounded-full bg-amber-100 text-amber-700 font-medium">Guest</span> <span className="px-1.5 py-0.5 text-[10px] rounded-full bg-amber-100 text-amber-700 font-medium">Guest</span>
)} )}
</div> </div>
@@ -1089,7 +1089,7 @@ export default function AdminEventDetailPage() {
</div> </div>
<div className="flex items-center gap-1.5 flex-shrink-0 flex-wrap justify-end"> <div className="flex items-center gap-1.5 flex-shrink-0 flex-wrap justify-end">
{getStatusBadge(ticket.status, true)} {getStatusBadge(ticket.status, true)}
{ticket.isGuest && ( {!!ticket.isGuest && (
<span className="px-1.5 py-0.5 text-[10px] rounded-full bg-amber-100 text-amber-700 font-medium">Guest</span> <span className="px-1.5 py-0.5 text-[10px] rounded-full bg-amber-100 text-amber-700 font-medium">Guest</span>
)} )}
</div> </div>

View File

@@ -281,6 +281,22 @@ export default function AdminPaymentsPage() {
}; };
}; };
// Hide pending-approval payments whose event has already ended.
// Fall back to startDatetime when endDatetime is absent; keep visible when we
// can't classify (event missing from list and no startDatetime on payment.event).
const visiblePendingApprovalPayments = (() => {
const now = new Date();
return pendingApprovalPayments.filter((payment) => {
const eventId = payment.event?.id;
const fullEvent = eventId ? events.find((e) => e.id === eventId) : undefined;
const endIso = fullEvent?.endDatetime
|| fullEvent?.startDatetime
|| payment.event?.startDatetime;
if (!endIso) return true;
return parseDate(endIso).getTime() >= now.getTime();
});
})();
// Get booking info for pending approval payments // Get booking info for pending approval payments
const getPendingBookingInfo = (payment: PaymentWithDetails) => { const getPendingBookingInfo = (payment: PaymentWithDetails) => {
if (!payment.ticket?.bookingId) { if (!payment.ticket?.bookingId) {
@@ -288,7 +304,7 @@ export default function AdminPaymentsPage() {
} }
// Count all pending payments with the same bookingId // Count all pending payments with the same bookingId
const bookingPayments = pendingApprovalPayments.filter( const bookingPayments = visiblePendingApprovalPayments.filter(
p => p.ticket?.bookingId === payment.ticket?.bookingId p => p.ticket?.bookingId === payment.ticket?.bookingId
); );
@@ -326,7 +342,7 @@ export default function AdminPaymentsPage() {
const paidBookingsCount = getUniqueBookingsCount( const paidBookingsCount = getUniqueBookingsCount(
payments.filter(p => p.status === 'paid') payments.filter(p => p.status === 'paid')
); );
const pendingApprovalBookingsCount = getUniqueBookingsCount(pendingApprovalPayments); const pendingApprovalBookingsCount = getUniqueBookingsCount(visiblePendingApprovalPayments);
if (loading) { if (loading) {
return ( return (
@@ -620,8 +636,8 @@ export default function AdminPaymentsPage() {
<div> <div>
<p className="text-sm text-gray-500">{locale === 'es' ? 'Pendientes de Aprobación' : 'Pending Approval'}</p> <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> <p className="text-xl font-bold text-yellow-600">{pendingApprovalBookingsCount}</p>
{pendingApprovalPayments.length !== pendingApprovalBookingsCount && ( {visiblePendingApprovalPayments.length !== pendingApprovalBookingsCount && (
<p className="text-xs text-gray-400">({pendingApprovalPayments.length} tickets)</p> <p className="text-xs text-gray-400">({visiblePendingApprovalPayments.length} tickets)</p>
)} )}
</div> </div>
</div> </div>
@@ -670,8 +686,8 @@ export default function AdminPaymentsPage() {
className={clsx('pb-3 px-1 text-sm font-medium border-b-2 transition-colors whitespace-nowrap min-h-[44px]', 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')}> activeTab === 'pending_approval' ? 'border-primary-yellow text-primary-dark' : 'border-transparent text-gray-500 hover:text-gray-700')}>
{locale === 'es' ? 'Pendientes' : 'Pending Approval'} {locale === 'es' ? 'Pendientes' : 'Pending Approval'}
{pendingApprovalPayments.length > 0 && ( {visiblePendingApprovalPayments.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">{visiblePendingApprovalPayments.length}</span>
)} )}
</button> </button>
<button onClick={() => setActiveTab('all')} <button onClick={() => setActiveTab('all')}
@@ -685,7 +701,7 @@ export default function AdminPaymentsPage() {
{/* Pending Approval Tab */} {/* Pending Approval Tab */}
{activeTab === 'pending_approval' && ( {activeTab === 'pending_approval' && (
<> <>
{pendingApprovalPayments.length === 0 ? ( {visiblePendingApprovalPayments.length === 0 ? (
<Card className="p-12 text-center"> <Card className="p-12 text-center">
<CheckCircleIcon className="w-12 h-12 text-green-400 mx-auto mb-4" /> <CheckCircleIcon className="w-12 h-12 text-green-400 mx-auto mb-4" />
<p className="text-gray-500"> <p className="text-gray-500">
@@ -696,7 +712,7 @@ export default function AdminPaymentsPage() {
</Card> </Card>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{pendingApprovalPayments.map((payment) => { {visiblePendingApprovalPayments.map((payment) => {
const bookingInfo = getPendingBookingInfo(payment); const bookingInfo = getPendingBookingInfo(payment);
return ( return (
<Card key={payment.id} className="p-4"> <Card key={payment.id} className="p-4">