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>
407 lines
21 KiB
TypeScript
407 lines
21 KiB
TypeScript
import { PaymentOptionsConfig } from '@/lib/api';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import clsx from 'clsx';
|
|
import {
|
|
CreditCardIcon,
|
|
BuildingLibraryIcon,
|
|
BoltIcon,
|
|
BanknotesIcon,
|
|
ArrowPathIcon,
|
|
CheckCircleIcon,
|
|
XCircleIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import type { PaymentOverridesController } from '../_hooks/usePaymentOverrides';
|
|
|
|
interface PaymentsTabProps {
|
|
locale: string;
|
|
payments: PaymentOverridesController;
|
|
}
|
|
|
|
export function PaymentsTab({ locale, payments }: PaymentsTabProps) {
|
|
const {
|
|
loadingPayments,
|
|
hasPaymentOverrides,
|
|
savingPayments,
|
|
globalPaymentOptions,
|
|
paymentOverrides,
|
|
getEffectivePaymentOption,
|
|
updatePaymentOverride,
|
|
handleResetToGlobal,
|
|
handleSavePaymentOptions,
|
|
} = payments;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{loadingPayments ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full" />
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
|
<div>
|
|
<h3 className="font-semibold text-base">
|
|
{locale === 'es' ? 'Métodos de Pago del Evento' : 'Event Payment Methods'}
|
|
</h3>
|
|
<p className="text-xs text-gray-500">
|
|
{hasPaymentOverrides
|
|
? (locale === 'es' ? 'Configuración personalizada' : 'Custom settings')
|
|
: (locale === 'es' ? 'Usando configuración global' : 'Using global settings')}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{hasPaymentOverrides && (
|
|
<Button variant="outline" size="sm" onClick={handleResetToGlobal} disabled={savingPayments} className="min-h-[44px] md:min-h-0">
|
|
<ArrowPathIcon className="w-4 h-4 mr-1.5" />
|
|
{locale === 'es' ? 'Resetear' : 'Reset'}
|
|
</Button>
|
|
)}
|
|
<Button size="sm" onClick={handleSavePaymentOptions} isLoading={savingPayments} className="min-h-[44px] md:min-h-0">
|
|
{locale === 'es' ? 'Guardar' : 'Save'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* TPago */}
|
|
<Card>
|
|
<div className="p-4 md:p-5">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
<CreditCardIcon className="w-4 h-4 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<h4 className="font-semibold text-sm">
|
|
{locale === 'es' ? 'TPago / Tarjeta' : 'TPago / Card'}
|
|
</h4>
|
|
<p className="text-[10px] text-gray-500">
|
|
{locale === 'es' ? 'Requiere aprobación' : 'Requires approval'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{globalPaymentOptions && !globalPaymentOptions.tpagoEnabled && (
|
|
<span className="text-[10px] text-gray-400 hidden sm:inline">
|
|
{locale === 'es' ? '(Deshabilitado global)' : '(Disabled globally)'}
|
|
</span>
|
|
)}
|
|
<button
|
|
onClick={() => updatePaymentOverride('tpagoEnabled', !getEffectivePaymentOption('tpagoEnabled'))}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
getEffectivePaymentOption('tpagoEnabled') ? 'bg-primary-yellow' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
getEffectivePaymentOption('tpagoEnabled') ? 'translate-x-6' : 'translate-x-1'
|
|
}`} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{getEffectivePaymentOption('tpagoEnabled') && (
|
|
<div className="space-y-3 pt-3 border-t">
|
|
<div>
|
|
<label className="block text-xs 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-[10px] text-gray-500 mb-2">
|
|
{locale === 'es'
|
|
? 'Cada enlace tiene un monto fijo. Un enlace distinto por cantidad de tickets.'
|
|
: 'Each link has a fixed amount. One link per 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-xs font-medium text-gray-600 w-20 flex-shrink-0">
|
|
{qty} {qty === 1 ? 'ticket' : 'tickets'}
|
|
</span>
|
|
<input
|
|
type="url"
|
|
value={(paymentOverrides[key] as string | null) ?? ''}
|
|
onChange={(e) => updatePaymentOverride(key, (e.target.value || null) as any)}
|
|
placeholder={(globalPaymentOptions?.[key] as string | null) || 'https://www.tpago.com.py/links?alias=...'}
|
|
className="flex-1 px-3 py-2 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">Instructions (EN)</label>
|
|
<textarea
|
|
value={paymentOverrides.tpagoInstructions ?? ''}
|
|
onChange={(e) => updatePaymentOverride('tpagoInstructions', e.target.value || null)}
|
|
rows={2}
|
|
placeholder={globalPaymentOptions?.tpagoInstructions || 'Instructions for users...'}
|
|
className="w-full px-3 py-2 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">Instrucciones (ES)</label>
|
|
<textarea
|
|
value={paymentOverrides.tpagoInstructionsEs ?? ''}
|
|
onChange={(e) => updatePaymentOverride('tpagoInstructionsEs', e.target.value || null)}
|
|
rows={2}
|
|
placeholder={globalPaymentOptions?.tpagoInstructionsEs || 'Instrucciones para usuarios...'}
|
|
className="w-full px-3 py-2 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p className="text-[10px] text-gray-400">
|
|
{locale === 'es' ? 'Vacío = configuración global' : 'Empty = global settings'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Bank Transfer */}
|
|
<Card>
|
|
<div className="p-4 md:p-5">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
<BuildingLibraryIcon className="w-4 h-4 text-green-600" />
|
|
</div>
|
|
<div>
|
|
<h4 className="font-semibold text-sm">
|
|
{locale === 'es' ? 'Transferencia Bancaria' : 'Bank Transfer'}
|
|
</h4>
|
|
<p className="text-[10px] text-gray-500">
|
|
{locale === 'es' ? 'Requiere aprobación' : 'Requires approval'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{globalPaymentOptions && !globalPaymentOptions.bankTransferEnabled && (
|
|
<span className="text-[10px] text-gray-400 hidden sm:inline">
|
|
{locale === 'es' ? '(Deshabilitado global)' : '(Disabled globally)'}
|
|
</span>
|
|
)}
|
|
<button
|
|
onClick={() => updatePaymentOverride('bankTransferEnabled', !getEffectivePaymentOption('bankTransferEnabled'))}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
getEffectivePaymentOption('bankTransferEnabled') ? 'bg-primary-yellow' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
getEffectivePaymentOption('bankTransferEnabled') ? 'translate-x-6' : 'translate-x-1'
|
|
}`} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{getEffectivePaymentOption('bankTransferEnabled') && (
|
|
<div className="space-y-3 pt-3 border-t">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
{[
|
|
{ label: locale === 'es' ? 'Banco' : 'Bank Name', key: 'bankName' as const, placeholder: 'e.g., Banco Itaú' },
|
|
{ label: locale === 'es' ? 'Titular' : 'Account Holder', key: 'bankAccountHolder' as const, placeholder: 'e.g., Juan Pérez' },
|
|
{ label: locale === 'es' ? 'N° Cuenta' : 'Account Number', key: 'bankAccountNumber' as const, placeholder: 'e.g., 1234567890' },
|
|
{ label: 'Alias', key: 'bankAlias' as const, placeholder: 'e.g., spanglish.pagos' },
|
|
{ label: locale === 'es' ? 'Teléfono' : 'Phone', key: 'bankPhone' as const, placeholder: '+595 981 123456' },
|
|
].map((field) => (
|
|
<div key={field.key}>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">{field.label}</label>
|
|
<input
|
|
type="text"
|
|
value={(paymentOverrides as any)[field.key] ?? ''}
|
|
onChange={(e) => updatePaymentOverride(field.key, e.target.value || null)}
|
|
placeholder={(globalPaymentOptions as any)?.[field.key] || field.placeholder}
|
|
className="w-full px-3 py-2 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">Notes (EN)</label>
|
|
<textarea
|
|
value={paymentOverrides.bankNotes ?? ''}
|
|
onChange={(e) => updatePaymentOverride('bankNotes', e.target.value || null)}
|
|
rows={2}
|
|
placeholder={globalPaymentOptions?.bankNotes || 'Additional notes...'}
|
|
className="w-full px-3 py-2 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">Notas (ES)</label>
|
|
<textarea
|
|
value={paymentOverrides.bankNotesEs ?? ''}
|
|
onChange={(e) => updatePaymentOverride('bankNotesEs', e.target.value || null)}
|
|
rows={2}
|
|
placeholder={globalPaymentOptions?.bankNotesEs || 'Notas adicionales...'}
|
|
className="w-full px-3 py-2 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p className="text-[10px] text-gray-400">
|
|
{locale === 'es' ? 'Vacío = configuración global' : 'Empty = global settings'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Bitcoin Lightning */}
|
|
<Card>
|
|
<div className="p-4 md:p-5">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
<BoltIcon className="w-4 h-4 text-orange-600" />
|
|
</div>
|
|
<div>
|
|
<h4 className="font-semibold text-sm">Bitcoin Lightning</h4>
|
|
<p className="text-[10px] text-gray-500">
|
|
{locale === 'es' ? 'Confirmación automática' : 'Auto confirmation'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{globalPaymentOptions && !globalPaymentOptions.lightningEnabled && (
|
|
<span className="text-[10px] text-gray-400 hidden sm:inline">
|
|
{locale === 'es' ? '(Deshabilitado global)' : '(Disabled globally)'}
|
|
</span>
|
|
)}
|
|
<button
|
|
onClick={() => updatePaymentOverride('lightningEnabled', !getEffectivePaymentOption('lightningEnabled'))}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
getEffectivePaymentOption('lightningEnabled') ? 'bg-primary-yellow' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
getEffectivePaymentOption('lightningEnabled') ? 'translate-x-6' : 'translate-x-1'
|
|
}`} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{getEffectivePaymentOption('lightningEnabled') && (
|
|
<div className="pt-3 border-t mt-3">
|
|
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3">
|
|
<p className="text-xs text-orange-800">
|
|
{locale === 'es'
|
|
? 'Lightning configurado vía LNbits. No personalizable por evento.'
|
|
: 'Lightning is configured via LNbits. Cannot be customized per event.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Cash at Door */}
|
|
<Card>
|
|
<div className="p-4 md:p-5">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
<BanknotesIcon className="w-4 h-4 text-yellow-600" />
|
|
</div>
|
|
<div>
|
|
<h4 className="font-semibold text-sm">
|
|
{locale === 'es' ? 'Efectivo' : 'Cash at Door'}
|
|
</h4>
|
|
<p className="text-[10px] text-gray-500">
|
|
{locale === 'es' ? 'Requiere aprobación' : 'Requires approval'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{globalPaymentOptions && !globalPaymentOptions.cashEnabled && (
|
|
<span className="text-[10px] text-gray-400 hidden sm:inline">
|
|
{locale === 'es' ? '(Deshabilitado global)' : '(Disabled globally)'}
|
|
</span>
|
|
)}
|
|
<button
|
|
onClick={() => updatePaymentOverride('cashEnabled', !getEffectivePaymentOption('cashEnabled'))}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
getEffectivePaymentOption('cashEnabled') ? 'bg-primary-yellow' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
getEffectivePaymentOption('cashEnabled') ? 'translate-x-6' : 'translate-x-1'
|
|
}`} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{getEffectivePaymentOption('cashEnabled') && (
|
|
<div className="space-y-3 pt-3 border-t">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">Instructions (EN)</label>
|
|
<textarea
|
|
value={paymentOverrides.cashInstructions ?? ''}
|
|
onChange={(e) => updatePaymentOverride('cashInstructions', e.target.value || null)}
|
|
rows={2}
|
|
placeholder={globalPaymentOptions?.cashInstructions || 'Cash payment instructions...'}
|
|
className="w-full px-3 py-2 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">Instrucciones (ES)</label>
|
|
<textarea
|
|
value={paymentOverrides.cashInstructionsEs ?? ''}
|
|
onChange={(e) => updatePaymentOverride('cashInstructionsEs', e.target.value || null)}
|
|
rows={2}
|
|
placeholder={globalPaymentOptions?.cashInstructionsEs || 'Instrucciones de pago en efectivo...'}
|
|
className="w-full px-3 py-2 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p className="text-[10px] text-gray-400">
|
|
{locale === 'es' ? 'Vacío = configuración global' : 'Empty = global settings'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Summary */}
|
|
<Card>
|
|
<div className="p-4 md:p-5">
|
|
<h4 className="font-semibold text-sm mb-3">
|
|
{locale === 'es' ? 'Resumen' : 'Active Methods'}
|
|
</h4>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
{[
|
|
{ label: 'TPago', enabled: getEffectivePaymentOption('tpagoEnabled') },
|
|
{ label: locale === 'es' ? 'Transferencia' : 'Bank Transfer', enabled: getEffectivePaymentOption('bankTransferEnabled') },
|
|
{ label: 'Lightning', enabled: getEffectivePaymentOption('lightningEnabled') },
|
|
{ label: locale === 'es' ? 'Efectivo' : 'Cash', enabled: getEffectivePaymentOption('cashEnabled') },
|
|
].map((method) => (
|
|
<div key={method.label} className="flex items-center gap-1.5">
|
|
{method.enabled ? (
|
|
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
|
) : (
|
|
<XCircleIcon className="w-4 h-4 text-gray-300" />
|
|
)}
|
|
<span className={clsx('text-sm', method.enabled ? 'text-gray-900' : 'text-gray-400')}>
|
|
{method.label}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{hasPaymentOverrides && (
|
|
<p className="text-[10px] text-gray-500 mt-3 flex items-center gap-1">
|
|
<span className="inline-block w-1.5 h-1.5 bg-primary-yellow rounded-full" />
|
|
{locale === 'es'
|
|
? 'Configuración personalizada activa'
|
|
: 'Custom settings override global defaults'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|