Fix booking defaults, dashboard alerts, admin edit flow, and ended-event payments.
Require an explicit payment method on booking, hide stale release banners for past or inactive events, open event editing in place on the detail page, and auto-reject unconfirmed payments after events end without sending email. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -335,7 +335,7 @@ export function BookingFormStep({
|
||||
<button
|
||||
key={method.id}
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, paymentMethod: method.id })}
|
||||
onClick={() => setFormData((prev) => ({ ...prev, paymentMethod: method.id }))}
|
||||
className={`w-full p-4 rounded-lg border-2 transition-all text-left flex items-start gap-4 ${
|
||||
formData.paymentMethod === method.id
|
||||
? 'border-primary-yellow bg-primary-yellow/10'
|
||||
@@ -377,6 +377,9 @@ export function BookingFormStep({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{errors.paymentMethod && (
|
||||
<p className="mt-3 text-sm text-red-600">{errors.paymentMethod}</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Terms & Privacy agreement */}
|
||||
@@ -430,7 +433,7 @@ export function BookingFormStep({
|
||||
size="lg"
|
||||
className="w-full"
|
||||
isLoading={submitting}
|
||||
disabled={paymentMethods.length === 0 || !agreedToTerms}
|
||||
disabled={paymentMethods.length === 0 || !formData.paymentMethod || !agreedToTerms}
|
||||
>
|
||||
{formData.paymentMethod === 'cash'
|
||||
? t('booking.form.reserveSpot')
|
||||
|
||||
@@ -13,7 +13,8 @@ export interface BookingFormData {
|
||||
email: string;
|
||||
phone: string;
|
||||
preferredLanguage: 'en' | 'es';
|
||||
paymentMethod: PaymentMethod;
|
||||
// Empty until the user explicitly picks a method (no default selection).
|
||||
paymentMethod: PaymentMethod | '';
|
||||
ruc: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function BookingPage() {
|
||||
email: '',
|
||||
phone: '',
|
||||
preferredLanguage: locale as 'en' | 'es',
|
||||
paymentMethod: 'cash',
|
||||
paymentMethod: '',
|
||||
ruc: '',
|
||||
});
|
||||
|
||||
@@ -130,18 +130,7 @@ export default function BookingPage() {
|
||||
return Array(need).fill(null).map((_, i) => prev[i] ?? { firstName: '', lastName: '' });
|
||||
});
|
||||
setPaymentConfig(paymentRes.paymentOptions);
|
||||
|
||||
// Set default payment method based on what's enabled
|
||||
const config = paymentRes.paymentOptions;
|
||||
if (config.lightningEnabled) {
|
||||
setFormData(prev => ({ ...prev, paymentMethod: 'lightning' }));
|
||||
} else if (config.cashEnabled) {
|
||||
setFormData(prev => ({ ...prev, paymentMethod: 'cash' }));
|
||||
} else if (config.bankTransferEnabled) {
|
||||
setFormData(prev => ({ ...prev, paymentMethod: 'bank_transfer' }));
|
||||
} else if (config.tpagoEnabled) {
|
||||
setFormData(prev => ({ ...prev, paymentMethod: 'tpago' }));
|
||||
}
|
||||
// No payment method is pre-selected; the user must choose one.
|
||||
})
|
||||
.catch(() => router.push('/events'))
|
||||
.finally(() => setLoading(false));
|
||||
@@ -216,6 +205,15 @@ export default function BookingPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Payment method must be explicitly chosen and currently enabled
|
||||
const availableMethods = buildPaymentMethods(paymentConfig, locale);
|
||||
if (
|
||||
!formData.paymentMethod ||
|
||||
!availableMethods.some((m) => m.id === formData.paymentMethod)
|
||||
) {
|
||||
newErrors.paymentMethod = t('booking.form.errors.paymentMethodRequired');
|
||||
}
|
||||
|
||||
// Validate additional attendees (if multi-ticket)
|
||||
attendees.forEach((attendee, index) => {
|
||||
if (!attendee.firstName.trim() || attendee.firstName.length < 2) {
|
||||
@@ -304,7 +302,7 @@ export default function BookingPage() {
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
preferredLanguage: formData.preferredLanguage,
|
||||
paymentMethod: formData.paymentMethod,
|
||||
paymentMethod: formData.paymentMethod as PaymentMethod,
|
||||
...(formData.ruc.trim() && { ruc: formData.ruc.replace(/\D/g, '') }),
|
||||
// Include attendees array for multi-ticket bookings
|
||||
...(allAttendees.length > 1 && { attendees: allAttendees }),
|
||||
@@ -366,7 +364,7 @@ export default function BookingPage() {
|
||||
bookingId,
|
||||
qrCode: primaryTicket.qrCode,
|
||||
qrCodes: ticketsList?.map((t: any) => t.qrCode),
|
||||
paymentMethod: formData.paymentMethod,
|
||||
paymentMethod: formData.paymentMethod as PaymentMethod,
|
||||
ticketCount,
|
||||
});
|
||||
setStep('success');
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
isUnpaid,
|
||||
isAwaitingApproval,
|
||||
isOnHold,
|
||||
isActionableAttention,
|
||||
ticketAmount,
|
||||
shareTicket,
|
||||
isToday,
|
||||
@@ -58,7 +59,9 @@ export default function OverviewTab({
|
||||
// awaiting approval), ordered by soonest event.
|
||||
const attentionTicket = useMemo(() => {
|
||||
const candidates = activeTickets.filter(
|
||||
(t) => isUnpaid(t) || isOnHold(t) || isAwaitingApproval(t)
|
||||
(t) =>
|
||||
isActionableAttention(t) &&
|
||||
(isUnpaid(t) || isOnHold(t) || isAwaitingApproval(t))
|
||||
);
|
||||
const priority = (t: UserTicket) => (isUnpaid(t) ? 0 : isOnHold(t) ? 1 : 2);
|
||||
candidates.sort((a, b) => {
|
||||
|
||||
@@ -35,6 +35,22 @@ export function isAwaitingApproval(ticket: { payment?: { status?: string } | nul
|
||||
return ticket.payment?.status === 'pending_approval';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an attention banner (e.g. "your spot was released", "payment pending")
|
||||
* is still worth showing to the user. It only makes sense to nudge the user when
|
||||
* the event still exists, is still bookable (published/unlisted), and has not
|
||||
* already ended. This suppresses stale banners for deleted, unpublished,
|
||||
* cancelled/completed/archived, or past events.
|
||||
*/
|
||||
export function isActionableAttention(ticket: UserTicket): boolean {
|
||||
const event = ticket.event;
|
||||
if (!event) return false;
|
||||
if (event.status !== 'published' && event.status !== 'unlisted') return false;
|
||||
const refDate = event.endDatetime || event.startDatetime;
|
||||
if (!refDate) return false;
|
||||
return parseDate(refDate).getTime() > Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* The moment the seat hold expires. null when paid/awaiting/cancelled or when
|
||||
* the data needed to compute it is missing.
|
||||
|
||||
Reference in New Issue
Block a user