feat: add payment management improvements and reminder emails
- Add option to approve/reject payments without sending notification emails (checkbox in review popup, default enabled) - Add payment reminder email template and send functionality - Track when reminder emails are sent (reminderSentAt field) - Display reminder sent timestamp in payment review popup - Make payment review popup scrollable for better UX - Add payment-reminder template to email system (available in admin emails)
This commit is contained in:
@@ -473,8 +473,8 @@ export default function BookingPage() {
|
||||
paymentMethods.push({
|
||||
id: 'tpago',
|
||||
icon: CreditCardIcon,
|
||||
label: locale === 'es' ? 'TPago / Tarjeta Internacional' : 'TPago / International Card',
|
||||
description: locale === 'es' ? 'Paga con tarjeta de crédito o débito' : 'Pay with credit or debit card',
|
||||
label: locale === 'es' ? 'TPago / Tarjetas de Crédito' : 'TPago / Credit Cards',
|
||||
description: locale === 'es' ? 'Pagá con tarjetas de crédito locales o internacionales' : 'Pay with local or international credit cards',
|
||||
badge: locale === 'es' ? 'Manual' : 'Manual',
|
||||
});
|
||||
}
|
||||
@@ -483,8 +483,8 @@ export default function BookingPage() {
|
||||
paymentMethods.push({
|
||||
id: 'bank_transfer',
|
||||
icon: BuildingLibraryIcon,
|
||||
label: locale === 'es' ? 'Transferencia Bancaria' : 'Bank Transfer',
|
||||
description: locale === 'es' ? 'Transferencia bancaria local' : 'Local bank transfer',
|
||||
label: locale === 'es' ? 'Transferencia Bancaria Local' : 'Local Bank Transfer',
|
||||
description: locale === 'es' ? 'Pago por transferencia bancaria en Paraguay' : 'Pay via Paraguayan bank transfer',
|
||||
badge: locale === 'es' ? 'Manual' : 'Manual',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
BanknotesIcon,
|
||||
BuildingLibraryIcon,
|
||||
CreditCardIcon,
|
||||
EnvelopeIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -38,6 +39,8 @@ export default function AdminPaymentsPage() {
|
||||
const [selectedPayment, setSelectedPayment] = useState<PaymentWithDetails | null>(null);
|
||||
const [noteText, setNoteText] = useState('');
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [sendEmail, setSendEmail] = useState(true);
|
||||
const [sendingReminder, setSendingReminder] = useState(false);
|
||||
|
||||
// Export state
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
@@ -77,10 +80,11 @@ export default function AdminPaymentsPage() {
|
||||
const handleApprove = async (payment: PaymentWithDetails) => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await paymentsApi.approve(payment.id, noteText);
|
||||
await paymentsApi.approve(payment.id, noteText, sendEmail);
|
||||
toast.success(locale === 'es' ? 'Pago aprobado' : 'Payment approved');
|
||||
setSelectedPayment(null);
|
||||
setNoteText('');
|
||||
setSendEmail(true);
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to approve payment');
|
||||
@@ -92,10 +96,11 @@ export default function AdminPaymentsPage() {
|
||||
const handleReject = async (payment: PaymentWithDetails) => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await paymentsApi.reject(payment.id, noteText);
|
||||
await paymentsApi.reject(payment.id, noteText, sendEmail);
|
||||
toast.success(locale === 'es' ? 'Pago rechazado' : 'Payment rejected');
|
||||
setSelectedPayment(null);
|
||||
setNoteText('');
|
||||
setSendEmail(true);
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to reject payment');
|
||||
@@ -104,6 +109,24 @@ export default function AdminPaymentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendReminder = async (payment: PaymentWithDetails) => {
|
||||
setSendingReminder(true);
|
||||
try {
|
||||
const result = await paymentsApi.sendReminder(payment.id);
|
||||
toast.success(locale === 'es' ? 'Recordatorio enviado' : 'Reminder sent');
|
||||
// Update the selected payment with the new reminderSentAt timestamp
|
||||
if (result.reminderSentAt) {
|
||||
setSelectedPayment({ ...payment, reminderSentAt: result.reminderSentAt });
|
||||
}
|
||||
// Also refresh the data to update the lists
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to send reminder');
|
||||
} finally {
|
||||
setSendingReminder(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmPayment = async (id: string) => {
|
||||
try {
|
||||
await paymentsApi.approve(id);
|
||||
@@ -317,7 +340,7 @@ export default function AdminPaymentsPage() {
|
||||
const modalBookingInfo = getBookingInfo(selectedPayment);
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-lg p-6">
|
||||
<Card className="w-full max-w-lg max-h-[90vh] overflow-y-auto p-6">
|
||||
<h2 className="text-xl font-bold mb-4">
|
||||
{locale === 'es' ? 'Verificar Pago' : 'Verify Payment'}
|
||||
</h2>
|
||||
@@ -374,6 +397,13 @@ export default function AdminPaymentsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPayment.reminderSentAt && (
|
||||
<div className="flex items-center gap-2 text-sm text-amber-600">
|
||||
<EnvelopeIcon className="w-4 h-4" />
|
||||
{locale === 'es' ? 'Recordatorio enviado:' : 'Reminder sent:'} {formatDate(selectedPayment.reminderSentAt)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPayment.payerName && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
<p className="text-sm text-amber-800 font-medium">
|
||||
@@ -395,6 +425,19 @@ export default function AdminPaymentsPage() {
|
||||
placeholder={locale === 'es' ? 'Agregar nota...' : 'Add a note...'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="sendEmail"
|
||||
checked={sendEmail}
|
||||
onChange={(e) => setSendEmail(e.target.checked)}
|
||||
className="w-4 h-4 text-primary-yellow border-gray-300 rounded focus:ring-primary-yellow"
|
||||
/>
|
||||
<label htmlFor="sendEmail" className="text-sm text-gray-700">
|
||||
{locale === 'es' ? 'Enviar email de notificación' : 'Send notification email'}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
@@ -417,8 +460,20 @@ export default function AdminPaymentsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleSendReminder(selectedPayment)}
|
||||
isLoading={sendingReminder}
|
||||
className="w-full"
|
||||
>
|
||||
<EnvelopeIcon className="w-5 h-5 mr-2" />
|
||||
{locale === 'es' ? 'Enviar recordatorio de pago' : 'Send payment reminder'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => { setSelectedPayment(null); setNoteText(''); }}
|
||||
onClick={() => { setSelectedPayment(null); setNoteText(''); setSendEmail(true); }}
|
||||
className="w-full mt-3 py-2 text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{locale === 'es' ? 'Cancelar' : 'Cancel'}
|
||||
|
||||
Reference in New Issue
Block a user