Files
Spanglish/frontend/src/app/(public)/book/[eventId]/_steps/ManualPaymentStep.tsx
T
MichilisandCursor 613bd7be1d Refactor monolithic modules and harden booking, email, and auth infrastructure.
Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 07:12:59 +00:00

250 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import {
CreditCardIcon,
BuildingLibraryIcon,
CheckCircleIcon,
ArrowTopRightOnSquareIcon,
} from '@heroicons/react/24/outline';
import { Event, PaymentOptionsConfig } from '@/lib/api';
import { formatPrice, getTpagoLink } from '@/lib/utils';
import type { BookingResult } from '../_types';
interface ManualPaymentStepProps {
bookingResult: BookingResult;
event: Event;
paymentConfig: PaymentOptionsConfig;
locale: string;
paidUnderDifferentName: boolean;
setPaidUnderDifferentName: (value: boolean) => void;
payerName: string;
setPayerName: (value: string) => void;
markingPaid: boolean;
onMarkPaymentSent: () => void;
}
export function ManualPaymentStep({
bookingResult,
event,
paymentConfig,
locale,
paidUnderDifferentName,
setPaidUnderDifferentName,
payerName,
setPayerName,
markingPaid,
onMarkPaymentSent,
}: ManualPaymentStepProps) {
const isBankTransfer = bookingResult.paymentMethod === 'bank_transfer';
const isTpago = bookingResult.paymentMethod === 'tpago';
const ticketCount = bookingResult.ticketCount || 1;
const totalAmount = (event?.price || 0) * ticketCount;
const tpagoLink = getTpagoLink(paymentConfig, ticketCount);
return (
<div className="section-padding">
<div className="container-page max-w-xl">
<Card className="p-6">
<div className="text-center mb-6">
<div className={`w-16 h-16 rounded-full ${isBankTransfer ? 'bg-green-100' : 'bg-blue-100'} flex items-center justify-center mx-auto mb-4`}>
{isBankTransfer ? (
<BuildingLibraryIcon className="w-8 h-8 text-green-600" />
) : (
<CreditCardIcon className="w-8 h-8 text-blue-600" />
)}
</div>
<h1 className="text-xl font-bold text-primary-dark mb-2">
{locale === 'es' ? 'Completa tu Pago' : 'Complete Your Payment'}
</h1>
<p className="text-gray-600">
{locale === 'es'
? 'Sigue las instrucciones para completar tu pago'
: 'Follow the instructions to complete your payment'}
</p>
</div>
{/* Amount to pay */}
<div className="bg-gray-50 rounded-lg p-4 mb-6 text-center">
<p className="text-sm text-gray-500 mb-1">
{locale === 'es' ? 'Monto a pagar' : 'Amount to pay'}
</p>
<p className="text-2xl font-bold text-primary-dark">
{event?.price !== undefined ? formatPrice(totalAmount, event.currency) : ''}
</p>
{ticketCount > 1 && (
<p className="text-sm text-gray-500 mt-1">
{ticketCount} tickets × {formatPrice(event?.price || 0, event?.currency || 'PYG')}
</p>
)}
</div>
{/* Bank Transfer Details */}
{isBankTransfer && (
<div className="space-y-4 mb-6">
<h3 className="font-semibold text-gray-900">
{locale === 'es' ? 'Datos Bancarios' : 'Bank Details'}
</h3>
<div className="bg-green-50 border border-green-200 rounded-lg p-4 space-y-3">
{paymentConfig.bankName && (
<div className="flex justify-between">
<span className="text-gray-600">{locale === 'es' ? 'Banco' : 'Bank'}:</span>
<span className="font-medium">{paymentConfig.bankName}</span>
</div>
)}
{paymentConfig.bankAccountHolder && (
<div className="flex justify-between">
<span className="text-gray-600">{locale === 'es' ? 'Titular' : 'Account Holder'}:</span>
<span className="font-medium">{paymentConfig.bankAccountHolder}</span>
</div>
)}
{paymentConfig.bankAccountNumber && (
<div className="flex justify-between">
<span className="text-gray-600">{locale === 'es' ? 'Nro. Cuenta' : 'Account Number'}:</span>
<span className="font-medium font-mono">{paymentConfig.bankAccountNumber}</span>
</div>
)}
{paymentConfig.bankAlias && (
<div className="flex justify-between">
<span className="text-gray-600">Alias:</span>
<span className="font-medium">{paymentConfig.bankAlias}</span>
</div>
)}
{paymentConfig.bankPhone && (
<div className="flex justify-between">
<span className="text-gray-600">{locale === 'es' ? 'Teléfono' : 'Phone'}:</span>
<span className="font-medium">{paymentConfig.bankPhone}</span>
</div>
)}
</div>
{(locale === 'es' ? paymentConfig.bankNotesEs : paymentConfig.bankNotes) && (
<p className="text-sm text-gray-600">
{locale === 'es' ? paymentConfig.bankNotesEs : paymentConfig.bankNotes}
</p>
)}
</div>
)}
{/* TPago Link */}
{isTpago && (
<div className="space-y-4 mb-6">
<h3 className="font-semibold text-gray-900">
{locale === 'es' ? 'Pago con Tarjeta' : 'Card Payment'}
</h3>
{tpagoLink && (
<a
href={tpagoLink}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 w-full px-6 py-4 bg-blue-600 text-white rounded-btn hover:bg-blue-700 transition-colors font-medium"
>
<ArrowTopRightOnSquareIcon className="w-5 h-5" />
{locale === 'es' ? 'Abrir TPago para Pagar' : 'Open TPago to Pay'}
</a>
)}
{(locale === 'es' ? paymentConfig.tpagoInstructionsEs : paymentConfig.tpagoInstructions) && (
<p className="text-sm text-gray-600">
{locale === 'es' ? paymentConfig.tpagoInstructionsEs : paymentConfig.tpagoInstructions}
</p>
)}
</div>
)}
{/* Reference */}
<div className="bg-gray-100 rounded-lg p-3 mb-6">
<p className="text-xs text-gray-500 mb-1">
{locale === 'es' ? 'Referencia de tu reserva' : 'Your booking reference'}
</p>
<p className="font-mono font-bold text-lg">{bookingResult.qrCode}</p>
</div>
{/* Manual verification notice */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<div className="flex gap-3">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-blue-600 mt-0.5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>
</div>
<div className="text-sm text-blue-800">
<p className="font-medium mb-1">
{locale === 'es' ? 'Verificación manual' : 'Manual verification'}
</p>
<p className="text-blue-700">
{locale === 'es'
? 'El equipo de Spanglish revisará el pago manualmente. Tu reserva solo será confirmada después de recibir un email de confirmación de nuestra parte.'
: 'The Spanglish team will review the payment manually. Your booking is only confirmed after you receive a confirmation email from us.'}
</p>
</div>
</div>
</div>
{/* Paid under different name option */}
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={paidUnderDifferentName}
onChange={(e) => {
setPaidUnderDifferentName(e.target.checked);
if (!e.target.checked) setPayerName('');
}}
className="mt-1 w-4 h-4 text-primary-yellow border-gray-300 rounded focus:ring-primary-yellow"
/>
<div>
<span className="font-medium text-gray-700">
{locale === 'es'
? 'El pago está a nombre de otra persona'
: 'The payment is under another person\'s name'}
</span>
<p className="text-xs text-gray-500 mt-1">
{locale === 'es'
? 'Marcá esta opción si el pago fue realizado por un familiar o tercero.'
: 'Check this option if the payment was made by a family member or a third party.'}
</p>
</div>
</label>
{paidUnderDifferentName && (
<div className="mt-3 pl-7">
<Input
label={locale === 'es' ? 'Nombre del pagador' : 'Payer name'}
value={payerName}
onChange={(e) => setPayerName(e.target.value)}
placeholder={locale === 'es' ? 'Nombre completo del titular de la cuenta' : 'Full name of account holder'}
required
/>
</div>
)}
</div>
{/* Warning before I Have Paid button */}
<p className="text-sm text-center text-amber-700 font-medium mb-3">
{locale === 'es'
? 'Solo haz clic aquí después de haber completado el pago.'
: 'Only click this after you have actually completed the payment.'}
</p>
{/* I Have Paid Button */}
<Button
onClick={onMarkPaymentSent}
isLoading={markingPaid}
size="lg"
className="w-full"
disabled={paidUnderDifferentName && !payerName.trim()}
>
<CheckCircleIcon className="w-5 h-5 mr-2" />
{locale === 'es' ? 'Ya Realicé el Pago' : 'I Have Paid'}
</Button>
<p className="text-xs text-center text-gray-500 mt-4">
{locale === 'es'
? 'Tu reserva será confirmada una vez que verifiquemos el pago'
: 'Your booking will be confirmed once we verify the payment'}
</p>
</Card>
</div>
</div>
);
}