Each ticket quantity (1-5) can have its own TPago link so checkout, emails, and admin config use the correct fixed-amount URL.
547 lines
23 KiB
TypeScript
547 lines
23 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useLanguage } from '@/context/LanguageContext';
|
|
import { paymentOptionsApi, PaymentOptionsConfig } from '@/lib/api';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import Input from '@/components/ui/Input';
|
|
import {
|
|
CreditCardIcon,
|
|
BanknotesIcon,
|
|
BoltIcon,
|
|
BuildingLibraryIcon,
|
|
CheckCircleIcon,
|
|
XCircleIcon,
|
|
ArrowPathIcon,
|
|
TicketIcon,
|
|
Cog6ToothIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import toast from 'react-hot-toast';
|
|
|
|
export default function PaymentOptionsPage() {
|
|
const { t, locale } = useLanguage();
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [options, setOptions] = useState<PaymentOptionsConfig>({
|
|
tpagoEnabled: false,
|
|
tpagoLink: null,
|
|
tpagoLink2: null,
|
|
tpagoLink3: null,
|
|
tpagoLink4: null,
|
|
tpagoLink5: null,
|
|
tpagoInstructions: null,
|
|
tpagoInstructionsEs: null,
|
|
bankTransferEnabled: false,
|
|
bankName: null,
|
|
bankAccountHolder: null,
|
|
bankAccountNumber: null,
|
|
bankAlias: null,
|
|
bankPhone: null,
|
|
bankNotes: null,
|
|
bankNotesEs: null,
|
|
lightningEnabled: true,
|
|
cashEnabled: true,
|
|
cashInstructions: null,
|
|
cashInstructionsEs: null,
|
|
allowDuplicateBookings: false,
|
|
});
|
|
|
|
useEffect(() => {
|
|
loadOptions();
|
|
}, []);
|
|
|
|
const loadOptions = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const { paymentOptions } = await paymentOptionsApi.getGlobal();
|
|
setOptions(paymentOptions);
|
|
} catch (error) {
|
|
console.error('Failed to load payment options:', error);
|
|
toast.error('Failed to load payment options');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
setSaving(true);
|
|
await paymentOptionsApi.updateGlobal(options);
|
|
toast.success('Payment options saved successfully');
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Failed to save payment options');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const updateOption = <K extends keyof PaymentOptionsConfig>(
|
|
key: K,
|
|
value: PaymentOptionsConfig[K]
|
|
) => {
|
|
setOptions((prev) => ({ ...prev, [key]: value }));
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-4xl">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-primary-dark">
|
|
{locale === 'es' ? 'Opciones de Pago' : 'Payment Options'}
|
|
</h1>
|
|
<p className="text-gray-600 mt-1">
|
|
{locale === 'es'
|
|
? 'Configura los métodos de pago disponibles para todos los eventos'
|
|
: 'Configure payment methods available for all events'}
|
|
</p>
|
|
</div>
|
|
<Button onClick={handleSave} isLoading={saving}>
|
|
{locale === 'es' ? 'Guardar Cambios' : 'Save Changes'}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* TPago / International Card */}
|
|
<Card className="mb-6">
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
|
<CreditCardIcon className="w-5 h-5 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-lg">
|
|
{locale === 'es' ? 'TPago / Tarjeta Internacional' : 'TPago / International Card'}
|
|
</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{locale === 'es' ? 'Pago manual - requiere aprobación' : 'Manual payment - requires approval'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => updateOption('tpagoEnabled', !options.tpagoEnabled)}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
options.tpagoEnabled ? 'bg-primary-yellow' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
options.tpagoEnabled ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{options.tpagoEnabled && (
|
|
<div className="space-y-4 pt-4 border-t">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
{locale === 'es' ? 'Enlaces de Pago TPago (por cantidad de tickets)' : 'TPago Payment Links (per ticket quantity)'}
|
|
</label>
|
|
<p className="text-xs text-gray-500 mb-2">
|
|
{locale === 'es'
|
|
? 'Cada enlace tiene un monto fijo. Usá un enlace distinto para cada cantidad de tickets.'
|
|
: 'Each link has a fixed amount. Use a different link for each ticket quantity.'}
|
|
</p>
|
|
<div className="space-y-2">
|
|
{([1, 2, 3, 4, 5] as const).map((qty) => {
|
|
const key = (qty === 1 ? 'tpagoLink' : `tpagoLink${qty}`) as keyof PaymentOptionsConfig;
|
|
return (
|
|
<div key={qty} className="flex items-center gap-2">
|
|
<span className="text-sm font-medium text-gray-600 w-24 flex-shrink-0">
|
|
{qty} {locale === 'es' ? (qty === 1 ? 'ticket' : 'tickets') : (qty === 1 ? 'ticket' : 'tickets')}
|
|
</span>
|
|
<Input
|
|
value={(options[key] as string | null) || ''}
|
|
onChange={(e) => updateOption(key, (e.target.value || null) as any)}
|
|
placeholder="https://www.tpago.com.py/links?alias=..."
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Instructions (English)
|
|
</label>
|
|
<textarea
|
|
value={options.tpagoInstructions || ''}
|
|
onChange={(e) => updateOption('tpagoInstructions', e.target.value || null)}
|
|
rows={3}
|
|
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
placeholder="Instructions for users..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Instrucciones (Español)
|
|
</label>
|
|
<textarea
|
|
value={options.tpagoInstructionsEs || ''}
|
|
onChange={(e) => updateOption('tpagoInstructionsEs', e.target.value || null)}
|
|
rows={3}
|
|
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
placeholder="Instrucciones para usuarios..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Bank Transfer */}
|
|
<Card className="mb-6">
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
|
<BuildingLibraryIcon className="w-5 h-5 text-green-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-lg">
|
|
{locale === 'es' ? 'Transferencia Bancaria' : 'Bank Transfer'}
|
|
</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{locale === 'es' ? 'Pago manual - requiere aprobación' : 'Manual payment - requires approval'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => updateOption('bankTransferEnabled', !options.bankTransferEnabled)}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
options.bankTransferEnabled ? 'bg-primary-yellow' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
options.bankTransferEnabled ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{options.bankTransferEnabled && (
|
|
<div className="space-y-4 pt-4 border-t">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
{locale === 'es' ? 'Nombre del Banco' : 'Bank Name'}
|
|
</label>
|
|
<Input
|
|
value={options.bankName || ''}
|
|
onChange={(e) => updateOption('bankName', e.target.value || null)}
|
|
placeholder="e.g., Banco Itaú"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
{locale === 'es' ? 'Titular de la Cuenta' : 'Account Holder'}
|
|
</label>
|
|
<Input
|
|
value={options.bankAccountHolder || ''}
|
|
onChange={(e) => updateOption('bankAccountHolder', e.target.value || null)}
|
|
placeholder="e.g., Juan Pérez"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
{locale === 'es' ? 'Número de Cuenta' : 'Account Number'}
|
|
</label>
|
|
<Input
|
|
value={options.bankAccountNumber || ''}
|
|
onChange={(e) => updateOption('bankAccountNumber', e.target.value || null)}
|
|
placeholder="e.g., 1234567890"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Alias
|
|
</label>
|
|
<Input
|
|
value={options.bankAlias || ''}
|
|
onChange={(e) => updateOption('bankAlias', e.target.value || null)}
|
|
placeholder="e.g., spanglish.pagos"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
{locale === 'es' ? 'Teléfono' : 'Phone Number'}
|
|
</label>
|
|
<Input
|
|
value={options.bankPhone || ''}
|
|
onChange={(e) => updateOption('bankPhone', e.target.value || null)}
|
|
placeholder="e.g., +595 981 123456"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Additional Notes (English)
|
|
</label>
|
|
<textarea
|
|
value={options.bankNotes || ''}
|
|
onChange={(e) => updateOption('bankNotes', e.target.value || null)}
|
|
rows={3}
|
|
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
placeholder="Additional notes for users..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Notas Adicionales (Español)
|
|
</label>
|
|
<textarea
|
|
value={options.bankNotesEs || ''}
|
|
onChange={(e) => updateOption('bankNotesEs', e.target.value || null)}
|
|
rows={3}
|
|
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
placeholder="Notas adicionales para usuarios..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Bitcoin Lightning */}
|
|
<Card className="mb-6">
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
|
<BoltIcon className="w-5 h-5 text-orange-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-lg">Bitcoin Lightning</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{locale === 'es' ? 'Pago instantáneo - confirmación automática' : 'Instant payment - automatic confirmation'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => updateOption('lightningEnabled', !options.lightningEnabled)}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
options.lightningEnabled ? 'bg-primary-yellow' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
options.lightningEnabled ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{options.lightningEnabled && (
|
|
<div className="pt-4 border-t">
|
|
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
|
|
<p className="text-sm text-orange-800">
|
|
{locale === 'es'
|
|
? 'Lightning está configurado a través de las variables de entorno de LNbits. Verifica que LNBITS_URL y LNBITS_API_KEY estén configurados correctamente.'
|
|
: 'Lightning is configured via LNbits environment variables. Make sure LNBITS_URL and LNBITS_API_KEY are properly set.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Cash at Door */}
|
|
<Card className="mb-6">
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
|
|
<BanknotesIcon className="w-5 h-5 text-yellow-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-lg">
|
|
{locale === 'es' ? 'Efectivo en el Evento' : 'Cash at the Door'}
|
|
</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{locale === 'es' ? 'Pago manual - requiere aprobación' : 'Manual payment - requires approval'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => updateOption('cashEnabled', !options.cashEnabled)}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
options.cashEnabled ? 'bg-primary-yellow' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
options.cashEnabled ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{options.cashEnabled && (
|
|
<div className="space-y-4 pt-4 border-t">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Instructions (English)
|
|
</label>
|
|
<textarea
|
|
value={options.cashInstructions || ''}
|
|
onChange={(e) => updateOption('cashInstructions', e.target.value || null)}
|
|
rows={3}
|
|
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
placeholder="Instructions for cash payments..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Instrucciones (Español)
|
|
</label>
|
|
<textarea
|
|
value={options.cashInstructionsEs || ''}
|
|
onChange={(e) => updateOption('cashInstructionsEs', e.target.value || null)}
|
|
rows={3}
|
|
className="w-full px-4 py-2 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
placeholder="Instrucciones para pagos en efectivo..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Booking Settings */}
|
|
<Card className="mb-6">
|
|
<div className="p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
|
|
<Cog6ToothIcon className="w-5 h-5 text-purple-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-lg">
|
|
{locale === 'es' ? 'Configuración de Reservas' : 'Booking Settings'}
|
|
</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{locale === 'es' ? 'Opciones adicionales para el proceso de reserva' : 'Additional options for the booking process'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4 pt-4 border-t">
|
|
{/* Allow Duplicate Bookings */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<TicketIcon className="w-5 h-5 text-gray-400" />
|
|
<div>
|
|
<p className="font-medium text-gray-900">
|
|
{locale === 'es' ? 'Permitir Reservas Múltiples' : 'Allow Multiple Bookings'}
|
|
</p>
|
|
<p className="text-sm text-gray-500">
|
|
{locale === 'es'
|
|
? 'Permitir que un usuario reserve varios tickets para el mismo evento con el mismo email'
|
|
: 'Allow a user to book multiple tickets for the same event with the same email'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => updateOption('allowDuplicateBookings', !options.allowDuplicateBookings)}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
options.allowDuplicateBookings ? 'bg-primary-yellow' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
options.allowDuplicateBookings ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{options.allowDuplicateBookings && (
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 ml-8">
|
|
<p className="text-sm text-yellow-800">
|
|
{locale === 'es'
|
|
? '⚠️ Cuando está habilitado, los usuarios pueden crear múltiples reservas para el mismo evento. Esto es útil para reservar en nombre de amigos o familiares.'
|
|
: '⚠️ When enabled, users can create multiple bookings for the same event. This is useful for booking on behalf of friends or family.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Summary */}
|
|
<Card>
|
|
<div className="p-6">
|
|
<h3 className="font-semibold text-lg mb-4">
|
|
{locale === 'es' ? 'Resumen de Métodos Activos' : 'Active Methods Summary'}
|
|
</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="flex items-center gap-2">
|
|
{options.tpagoEnabled ? (
|
|
<CheckCircleIcon className="w-5 h-5 text-green-500" />
|
|
) : (
|
|
<XCircleIcon className="w-5 h-5 text-gray-300" />
|
|
)}
|
|
<span className={options.tpagoEnabled ? 'text-gray-900' : 'text-gray-400'}>
|
|
TPago
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{options.bankTransferEnabled ? (
|
|
<CheckCircleIcon className="w-5 h-5 text-green-500" />
|
|
) : (
|
|
<XCircleIcon className="w-5 h-5 text-gray-300" />
|
|
)}
|
|
<span className={options.bankTransferEnabled ? 'text-gray-900' : 'text-gray-400'}>
|
|
{locale === 'es' ? 'Transferencia' : 'Bank Transfer'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{options.lightningEnabled ? (
|
|
<CheckCircleIcon className="w-5 h-5 text-green-500" />
|
|
) : (
|
|
<XCircleIcon className="w-5 h-5 text-gray-300" />
|
|
)}
|
|
<span className={options.lightningEnabled ? 'text-gray-900' : 'text-gray-400'}>
|
|
Lightning
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{options.cashEnabled ? (
|
|
<CheckCircleIcon className="w-5 h-5 text-green-500" />
|
|
) : (
|
|
<XCircleIcon className="w-5 h-5 text-gray-300" />
|
|
)}
|
|
<span className={options.cashEnabled ? 'text-gray-900' : 'text-gray-400'}>
|
|
{locale === 'es' ? 'Efectivo' : 'Cash'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Save button (bottom) */}
|
|
<div className="mt-6 flex justify-end">
|
|
<Button onClick={handleSave} isLoading={saving} size="lg">
|
|
{locale === 'es' ? 'Guardar Cambios' : 'Save Changes'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|