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>
250 lines
11 KiB
TypeScript
250 lines
11 KiB
TypeScript
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>
|
||
);
|
||
}
|