first commit
This commit is contained in:
461
frontend/src/app/admin/payment-options/page.tsx
Normal file
461
frontend/src/app/admin/payment-options/page.tsx
Normal file
@@ -0,0 +1,461 @@
|
||||
'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,
|
||||
} 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,
|
||||
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,
|
||||
});
|
||||
|
||||
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' ? 'Enlace de Pago TPago' : 'TPago Payment Link'}
|
||||
</label>
|
||||
<Input
|
||||
value={options.tpagoLink || ''}
|
||||
onChange={(e) => updateOption('tpagoLink', e.target.value || null)}
|
||||
placeholder="https://www.tpago.com.py/links?alias=..."
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user